npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mytil67/school-map

v0.12.0

Published

Référentiel d'écoles et carte réutilisables — écoles géocodées de Strasbourg, circonscriptions, territoires, annuaire des élus, et une carte dont les marqueurs sont pilotés par les données.

Readme

@mytil67/school-map

Référentiel des écoles de Strasbourg et carte réutilisable, pour les applications qui parlent du parc scolaire.

npm install @mytil67/school-map leaflet react-leaflet

Publié sur npm public : ni jeton, ni .npmrc, ni variable d'environnement — en local comme en déploiement.

Ce que le paquet contient

| | | | --- | --- | | 116 écoles | nom, type, adresse, coordonnées, UAI, téléphone | | les anciens noms | résolus vers l'école actuelle, plutôt que perdus | | 6 territoires | les secteurs de la ville, avec leur couleur d'identité | | 7 circonscriptions | la grille pédagogique de l'Éducation nationale | | 13 adjoint·es de quartier | quartiers, écoles couvertes, assistantes | | 38 quartiers | et l'adjoint·e qui en répond | | éducation prioritaire | REP, REP+, QPV, et l'IEN de chaque école | | la carte scolaire | 50 secteurs élémentaires, 59 maternelles, chacun rattaché à ses écoles | | les zones | périmètres des adjoint·es, QPV, territoires et circonscriptions tracés | | un composant de carte | marqueurs et zones pilotés par les données, popup libre |

Les données se resynchronisent depuis deux portails d'open data : lieux_education de la Ville de Strasbourg et l'annuaire de l'éducation.

Pourquoi un paquet

La même carte avait été réécrite dans chaque application, chaque fois soudée à son hôte : useNavigate de react-router, les types du domaine local, les couleurs du système de design en dur. Ici, rien de tout ça :

  • aucune dépendance à un routeur — onSelect remonte le point cliqué ;
  • aucune dépendance à un système de design — la popup est un render prop, l'application la met en forme comme elle l'entend ;
  • aucune couleur imposée — couleur et rayon viennent de chaque point.

La carte

import 'leaflet/dist/leaflet.css';   // ← à la racine de l'application
import { SchoolMap, STRASBOURG_SCHOOLS } from '@mytil67/school-map';

<SchoolMap
  points={STRASBOURG_SCHOOLS.map(ecole => ({
    id: ecole.slug ?? ecole.name,
    name: ecole.name,
    lat: ecole.lat,
    lon: ecole.lon,
    color: '#5b5bd6',
    radius: 8,
  }))}
  height={500}
  fitToPoints
  legend={[{ label: 'Écoles', color: '#5b5bd6' }]}
  renderPopup={point => <Fiche ecole={point.data} />}
  onSelect={point => ouvrir(point.data)}
/>

La feuille de style de Leaflet est à votre charge. Le paquet ne l'importe pas : un import de CSS depuis le JS le rendrait inutilisable hors bundler (Node, rendu serveur, scripts). En développement, la carte vous avertit en console si elle manque — sans elle, les tuiles s'empilent n'importe où, sans erreur.

leaflet, react-leaflet, react et react-dom sont des peerDependencies. Deux copies de Leaflet dans un même bundle cassent les icônes sans message : npm ls leaflet ne doit en montrer qu'une.

Les trois découpages

Une école appartient à trois grilles qui ne se recouvrent pas : sa circonscription d'inspection, son territoire ville, et ses adjoint·es de quartier. Tout est accessible depuis la racine :

import {
  findCirconscription, findElusForSchool, findTerritory,
} from '@mytil67/school-map';

findTerritory('Sturm II Élémentaire');       // { id: 'ESCRO', label: 'Esplanade…', color: '#10b981' }
findCirconscription('Sturm II Élémentaire'); // { id: '7', … }
findElusForSchool('Sturm II Élémentaire');   // [{ name: 'MORITZ Hervé', assistant: … }]

La couleur suit le territoire, jamais sa position dans un tri : un même secteur ne change pas de teinte selon le filtre actif.

Les adjoint·es de quartier

Les 13 adjoint·es de quartier de l'annuaire municipal, avec leurs quartiers, leurs écoles et leurs assistantes. L'annuaire mêle deux rôles : les 18 élu·es qui siègent en conseil d'école sans porter ce rôle ne sont pas des référent·es territoriaux, et ne sont pas au paquet. Les confondre donnait à une école cinq « référents » dont un seul en était un.

L'annuaire se lit dans les deux sens :

import {
  DISTRICTS, ELUS, SCHOOLS_WITH_ELUS,
  findEluByName, findElusByDistrict, findElusForSchool, findSchoolsOfElu,
} from '@mytil67/school-map';

findElusForSchool('Sturm II Élémentaire');  // qui est référent de cette école ?
findSchoolsOfElu('Hervé Moritz');           // → SchoolLocation[], géocodées
findElusByDistrict('Grande île');           // → Elu[]

findEluByName et findElusByDistrict tolèrent la casse, les accents et l'ordre prénom/nom — « herve moritz » trouve « MORITZ Hervé ». En cas d'ambiguïté ils renvoient null plutôt que de deviner.

Pour un tableau ou une carte, partez de SCHOOLS_WITH_ELUS : les 116 écoles, chacune avec ses référent·es, son territoire et sa circonscription, jointes une fois pour toutes.

<SchoolMap
  points={SCHOOLS_WITH_ELUS.map(({ school, elus, territory }) => ({
    id: school.slug ?? school.name,
    name: school.name,
    lat: school.lat,
    lon: school.lon,
    color: territory?.color ?? TERRITORY_FALLBACK_COLOR,
    data: { school, elus },
  }))}
  renderPopup={({ data }) => <Referents elus={data.elus} />}
  fitToPoints
/>

Boucler sur findElusForSchool à la place relancerait 116 rapprochements flous sur des noms qui sont déjà ceux du référentiel.

elus est souvent vide, et c'est le cas courant : l'annuaire ne rattache que 65 des 116 écoles à un·e adjoint·e. Cela veut dire que l'annuaire ne la mentionne pas, jamais qu'elle n'a pas de référent·e — ne l'affichez pas comme un manquement. La liste peut aussi porter plusieurs noms : les quartiers se chevauchent.

DISTRICTS donne les 38 quartiers cités, chacun avec ses adjoint·es — mais sans liste d'écoles : l'annuaire rattache les écoles à une personne, pas à un quartier. Une adjointe couvrant six quartiers et quatre écoles, répartir les secondes sur les premiers demanderait d'inventer un rattachement que la source ne donne pas.

ELUS_SOURCE_DATE porte la date de l'annuaire embarqué.

Les zones

Des tracés qui découpent la ville, à poser sous les écoles. Ils vivent dans leur propre sous-chemin — ~290 Ko, ~70 Ko compressés — que la racine ne réexporte pas : qui n'affiche que des points ne les paie pas.

import { SchoolMap } from '@mytil67/school-map';
import { SCHOOL_SECTORS, colorZones } from '@mytil67/school-map/zones';

<SchoolMap
  points={points}
  zones={colorZones(SCHOOL_SECTORS.elementaire)}
  renderZonePopup={zone => <>{zone.name}</>}
  fitToPoints
/>

| Export | Zones | Source | | --- | --- | --- | | SCHOOL_SECTORS.elementaire | 50 | carte scolaire de la Ville | | SCHOOL_SECTORS.maternelle | 59 | carte scolaire de la Ville | | ELU_ZONES | 13 | périmètres des adjoint·es de quartier 2026 | | QPV_ZONES | 15 | quartiers prioritaires 2024-2030, à Strasbourg | | TERRITORY_ZONES | 6 | reconstruits | | CIRCONSCRIPTION_ZONES | 7 | reconstruites |

Chaque secteur nomme ses écoles (schools, noms du référentiel) : le SIG municipal écrit « ERC.CHATRIAN » ou « ST JEAN », le rapprochement est fait une fois pour toutes. Un secteur de site en couvre plusieurs — « REUSS » dessert Reuss 1 et Reuss 2. Les écoles européenne et internationale n'ont pas de secteur : elles recrutent hors carte scolaire.

Territoires et circonscriptions n'ont pas de contour publié. Ils sont reconstruits comme réunion des secteurs élémentaires de leurs écoles : c'est « ce que desservent ses écoles », pas une limite administrative. Ils portent derived: true — montrez-les en pointillés (dashed). Le jeu « directions de territoire » de la Ville n'est pas utilisé : il compte 7 zones et ne découpe pas la ville comme les 6 territoires.

colorZones colore une mosaïque pour que deux zones voisines n'aient jamais la même teinte — avec une palette qui tourne, deux secteurs mitoyens finissent tôt ou tard de la même couleur et leur limite disparaît. Cinq teintes suffisent aux cartes scolaires ; les territoires, eux, gardent leur couleur d'identité.

Les tracés servent aussi hors carte :

import { findSectorAt, findSectorOfSchool } from '@mytil67/school-map/zones';

findSectorAt(48.5712, 7.7521, 'elementaire'); // de quelle école dépend cette adresse ?
findSectorOfSchool('EE Guynemer 1');           // → secteur « GUYNEMER 1 & 2 »

pointInZone(lat, lon, geometry) et findZoneAt(lat, lon, zones) marchent sur n'importe quelles zones, celles de l'application comprises.

Le périmètre d'un groupe d'écoles

zoneOfSchools dessine ce que couvre un ensemble d'écoles : la réunion de leurs secteurs, d'un seul tenant, sans les frontières intérieures. Le territoire d'un·e RTT, d'un·e RET, d'un·e IEN — tout rattachement d'écoles à une personne, calculé à la demande depuis vos propres données :

import { zoneOfSchools } from '@mytil67/school-map/zones';

const { geometry, sectors, unplaced } = zoneOfSchools(slugsDesEcolesDuRTT);

<SchoolMap points={points} zones={[{ id: 'rtt', name: 'Périmètre', geometry }]} />

Les écoles se désignent par slug, par nom, ou par n'importe quelle écriture d'usage. unplaced rend celles qu'aucun secteur ne couvre — nom inconnu, école hors carte scolaire : les taire dessinerait un périmètre plus petit sans le dire.

Un seul découpage à la fois : grid: 'elementaire' par défaut, ou 'maternelle'. Une école de l'autre cycle compte pour le secteur de la grille où elle se trouve.

mergeZones(zones) fait la même fusion sur n'importe quelle liste de zones d'un même découpage — secteurs, périmètres d'élu·es. Elle ne calcule pas d'union générale : deux zones voisines partagent exactement leur frontière, et une arête présente dans les deux sens est intérieure. C'est rapide (quelques millisecondes pour une vingtaine d'écoles) et sans dépendance — mais deux découpages différents, maternelle et élémentaire, ne se fusionnent pas entre eux.

La géométrie est en GeoJSON : longitude d'abord, l'inverse de MapPoint. Les fonctions, elles, prennent toujours la latitude en premier.

Trois écoles sont posées hors du secteur qui les nomme sur les tracés mêmes de la Ville : Canardière (11 m) et Marguerite Perey (5 m), dont le point tombe de l'autre côté de la rue limite, et Conseil des XV cycle 3 (112 m). findSectorAt à leur adresse répond donc le secteur voisin — c'est ce que dit la carte scolaire, pas une erreur du paquet.

Le portrait environnemental

Ce que l'open data sait de l'entourage de chaque école, calculé une fois à la synchronisation pour que personne n'ait à le saisir :

import { findEnvironment } from '@mytil67/school-map/environment';

findEnvironment('ecole-elementaire-du-conseil-des-xv-cycle-2');
// {
//   roadNoise: '55-60',                          // dB(A) Lden, carte 2022
//   flood: { overflow: null, groundwater: 'Zone jaune' },   // PPRi
//   tram: { stop: "PLACE D'ISLANDE", lines: 'F', meters: 536 },
//   gym:  { name: 'Salle omnisports du Centre Sportif Bon Pasteur', meters: 329 },
//   pool: { name: 'piscine de la Kibitzenau', meters: … },
//   canopy: 17.9,                                // % d'arbres ≥ 3 m dans 100 m
// }

| Champ | Source | À savoir | | --- | --- | --- | | roadNoise | carte de bruit routier 2022 | classe Lden en dB(A) ; au-delà de 68, point noir du bruit | | flood.overflow | PPRi, débordement | 2 écoles concernées | | flood.groundwater | PPRi, remontée de nappe | presque toute la ville : 112 écoles en « Zone jaune » | | tram | stations de tram | la plus proche, et ses lignes | | gym | équipements sportifs | salle omnisports la plus proche | | pool | équipements sportifs | piscine couverte la plus proche — bassins d'été écartés | | canopy | LIDAR 2025, 0,5 m | part du disque de 100 m couverte d'arbres de 3 m ou plus |

Rien n'est interprété : une zone de PPRi garde son nom réglementaire, une classe de bruit sa plage. null pour une zone de PPRi veut dire « hors zone réglementée pour cet aléa », jamais « sans risque ». Les distances sont à vol d'oiseau. canopy mesure les arbres du quartier, pas ceux de la cour seule.

Les jeux sources pèsent 50 Mo pour le bruit et 1,85 Go pour le raster ; seuls les attributs calculés, ~40 Ko, entrent dans le paquet. Le raster n'est jamais téléchargé : on n'en lit que le carré de 200 m autour de chaque école.

Les données, sans la carte

Deux sous-chemins tirent les données seules — ni Leaflet, ni DOM. Un script Node, un traitement serveur ou un rendu côté serveur doivent passer par là ; la racine, elle, importe Leaflet :

import { findSchoolLocation } from '@mytil67/school-map/referential';
import { findElusForSchool } from '@mytil67/school-map/territories';

Brancher ses propres données

Votre application tient un tableau qui parle des écoles — effectifs, équipements, contacts — nommées à sa façon. joinToSchools le rapproche du référentiel :

import { joinToSchools } from '@mytil67/school-map/referential';
import lignes from './data/mon-tableau.json';   // reste chez vous

const jointure = joinToSchools(lignes, { name: 'Nom école', type: 'Type' });

console.log(jointure.report());
jointure.get('EE Sturm II');          // les lignes de cette école
jointure.unmatched;                   // ce qui n'a PAS été rapproché
jointure.schoolsWithoutRow;           // les écoles sans aucune ligne

Le paquet ne voit jamais le contenu de vos lignes. T reste le vôtre, les objets sont rendus intacts, rien n'est copié dans le paquet. C'est le rapprochement de noms qui est mutualisé, pas la donnée.

Passez la colonne de type si vous l'avez. C'est le même piège qu'ailleurs : « Saint Thomas » existe en maternelle et en élémentaire, à la même adresse ; sans indice, une ligne sur deux tombe sur la mauvaise.

report(), unmatched et schoolsWithoutRow existent pour que rien ne disparaisse en silence — une ligne non rapprochée est une école absente du tableau final, sans le moindre message.

Pour un tableau qui parle de sites et non d'écoles — où « Schoepflin » désigne les deux — passez spread: true : une ligne peut alors couvrir plusieurs écoles.

Rapprocher un nom

Chaque application nomme ses écoles à sa façon. findSchoolLocation ramène ces écritures sur la même entrée :

| Écriture | Source | | --- | --- | | École élémentaire Jacques Sturm | le référentiel, d'après l'open data de la Ville | | Sturm II Élémentaire | ZAPI, reconstruit depuis les groupes JAMF | | EE Sturm II | exports bruts | | Elémentaire Jacques Sturm | annuaire municipal des élus |

Quatre passes, de la plus sûre à la plus permissive : égalité stricte, égalité des noms propres une fois retirés qualificatifs (école, maternelle, EE…) et rattachements (et ALM, d'application), comparaison mot à mot, puis inclusion. Les numéros de bâtiment restent bloquants — Guynemer 1 n'est pas Guynemer 2 — mais un libellé sans numéro couvre tout le site.

Les noms d'usage retirés du référentiel continuent de résoudre : Ecole Neuhof A maternelle et ALM ramène sur Alice Mosnier, dont elle était un doublon. Ils sont listés dans src/data/aliases.json et consultés après l'égalité stricte — une vraie entrée l'emporte toujours.

Passez le type quand vous le connaissez. Une dizaine d'écoles existent en maternelle et en élémentaire sous le même nom propre, souvent à la même adresse ; sans indice, une fois sur deux c'est la mauvaise qui répond :

findSchoolLocation('Saint Thomas', { type: 'maternelle' });

API

| Export | Rôle | | --- | --- | | SchoolMap | le composant de carte | | STRASBOURG_SCHOOLS | les 116 écoles | | findSchoolLocation(nom, opts) | rapproche un nom d'une entrée | | findAllSchoolLocations(nom, opts) | toutes les entrées qu'un nom peut désigner — « Schoepflin » en couvre deux | | normalizeSchoolName(nom) | clé de comparaison (minuscules, sans accent ni séparateur) | | inferSchoolType(nom) | maternelle | elementaire | unknown | | createMarkerIcon(couleur, rayon) | la pastille, pour monter sa propre carte | | joinToSchools(lignes, opts) | rapproche votre tableau du référentiel, et rend compte des deux bords |

Les zones, depuis /zones seulement : SCHOOL_SECTORS, ELU_ZONES, QPV_ZONES, TERRITORY_ZONES, CIRCONSCRIPTION_ZONES, ZONES_SOURCE_DATE, ZONES_SOURCES, ZONE_PALETTE, colorZones, pointInZone, findZoneAt, findSectorAt, findSectorOfSchool, zoneOfSchools, mergeZones.

Le portrait, depuis /environment : SCHOOL_ENVIRONMENT, findEnvironment, ENVIRONMENT_SOURCE_DATE, ENVIRONMENT_SOURCES.

Les trois découpages, depuis la racine ou depuis /territories : CIRCONSCRIPTIONS, TERRITORIES, DISTRICTS, ELUS, SCHOOLS_WITH_ELUS, ELUS_SOURCE_DATE, TERRITORY_COLORS, TERRITORY_FALLBACK_COLOR, findCirconscription, findTerritory, findElusForSchool, findEluByName, findSchoolsOfElu, findElusByDistrict.

Propriétés d'une école : name, type, address, lat, lon, territory, circonscription, uai, slug, officialName, phone, normalized, source, rep, qpv, ien.

rep vaut 'REP', 'REP+' ou null, et qpv true ou null. null n'est pas false : il dit « hors REP » ou « non déclaré », jamais « renseigné comme non ». undefined signale les 2 écoles que le classeur municipal ne liste pas.

slug est la clé de rendu à privilégier. Contrairement à l'UAI — que 13 groupes scolaires partagent entre leur maternelle et leur élémentaire — il désigne une école et une seule.

Props de SchoolMap : points, zones, center, zoom, height, fitToPoints, fitTo, renderPopup, onSelect, renderZonePopup, onSelectZone, zoneTooltips, legend, tileUrl, tileAttribution, exportSafe, ariaLabel, className, id.

Une zone (MapZone) : id, name, geometry (Polygon ou MultiPolygon GeoJSON), color, fillOpacity, dashed, data. Une entrée de légende prend shape: 'area' pour une zone.

exportSafe prépare la carte pour une capture html2canvas : rendu canvas et tuiles en cross-origin, au prix d'un peu de fluidité.

La carte expose aussi la liste de ses points en texte masqué : une carte Leaflet est un mur pour un lecteur d'écran.

Quand ça ne marche pas

| Symptôme | Cause la plus probable | | --- | --- | | Carte grise, tuiles empilées n'importe où | import 'leaflet/dist/leaflet.css' oublié | | Carte de hauteur nulle | le conteneur parent n'a pas de hauteur : passez height en pixels, pas en % | | Icônes de marqueurs cassées | deux copies de Leaflet dans le bundle — npm ls leaflet ne doit en montrer qu'une | | window is not defined | vous importez la racine côté serveur : elle tire Leaflet. Passez par /referential ou /territories, qui n'ont besoin ni de DOM ni de Leaflet | | Une école n'apparaît pas | elle n'est pas au référentiel : comptez et affichez les non-localisées plutôt que de les laisser disparaître | | Une école n'a aucun·e adjoint·e | normal pour 51 des 116 : l'annuaire ne les mentionne pas. Ce n'est pas un manquement |

Le journal des versions est livré avec le paquet : node_modules/@mytil67/school-map/CHANGELOG.md.

Développement

npm install
npm run build     # tsup : ESM + CJS + types
npm run check     # node --test : 107 tests, dont les 351 écritures de noms
npm run data      # resynchronise les deux open data et rejoue tout