Polices de cartes : typographie cartographique, glyphs SDF et compromis MapLibre
Guide complet et sourcé des polices pour cartes vectorielles : ce qu'est un glyph SDF et un fontstack, quelles polices les fournisseurs utilisent vraiment (Noto, Roboto, SF Pro, DIN), les conventions de typographie cartographique (hiérarchie, italique pour l'eau, halos), les compromis (couverture vs poids, glyphs servis vs polices locales, CJK) et comment générer ses propres glyphs.
Quand on dit « la police d’une carte », on parle en fait de deux problèmes qui n’ont presque rien à voir. Le premier est cartographique : quelle typographie choisir, comment hiérarchiser les labels, pourquoi l’eau est en italique. Le second est technique : comment un moteur de rendu vectoriel comme MapLibre GL affiche réellement du texte — pas avec un font-family CSS, mais avec des glyphs pré-calculés en champ de distance signée (SDF) servis en Protobuf. Les deux se rejoignent dans une poignée de propriétés text-* d’un style, mais les arbitrages qu’ils imposent sont distincts.
Cet article complète la série cartographie du blog. Self-héberger OpenFreeMap couvre la production des tuiles, les sprites d’icônes MapLibre couvre l’atlas d’icônes, et éditer un fond de carte vectoriel a identifié les trois ressources externes d’un style :
sources,spriteetglyphs. Cet article détaille la troisième — celle qu’on branche sans jamais la regarder.
1. Le texte n’est pas une donnée de la tuile
Premier contre-intuitif : le texte des labels n’est pas rasterisé dans les tuiles. Une tuile vectorielle contient des géométries et des attributs (name, name:fr, name:en, class, rank…). C’est le moteur de rendu, côté client, qui décide quel attribut afficher, à quelle taille, avec quelle police, et qui place le label pour éviter les collisions.
Conséquence directe : changer la police d’une carte vectorielle ne demande pas de re-générer les tuiles. On modifie le style (la propriété text-font des couches) et on pointe glyphs vers un serveur qui expose la nouvelle police. C’est ce qui distingue une carte vectorielle d’une carte raster (tuiles PNG) comme le rendu historique d’OpenStreetMap, où la police est cuite dans l’image au moment du rendu serveur.
Carte raster (OSM Carto) Carte vectorielle (MapLibre GL)
───────────────────────── ───────────────────────────────
tuile PNG tuile .pbf (géométries + name)
│ texte déjà dessiné │ pas de texte dessiné
▼ (police cuite serveur) ▼
affichage moteur GL place + dessine le label
│ ← text-font (fontstack)
│ ← glyphs (SDF PBF)
▼
affichage
La police d’une carte raster est un choix serveur définitif ; celle d’une carte vectorielle est un choix client, modifiable à la volée.
2. Comment MapLibre affiche du texte : glyphs, SDF et fontstack
2.1 Le champ de distance signée (SDF)
WebGL et OpenGL ES n’ont pas de rendu de texte natif. Pour dessiner un « A » net à n’importe quelle taille, rotation et avec un halo, Mapbox a introduit en 2014 le rendu par champ de distance signée (Signed Distance Field). Chaque glyph est pré-calculé non pas comme une image du caractère, mais comme une grille où chaque pixel encode la distance au bord de la forme. Un shader reconstruit ensuite le contour net en comparant cette distance à un seuil — et, en décalant le seuil, produit gratuitement le halo, le flou et le contour [Source : A History of Map Glyphs, Fonts, and SDFs — VonCannon].
C’est pour ça que les labels MapLibre restent nets quand on zoome entre deux niveaux, tournent avec la carte (labels de rues) et portent un halo sans que le fournisseur ait à générer une image par taille.
Le compromis est dans la génération : les SDF sont cuits à une taille fixe (l’outil de référence, sdf-glyph-foundry/node-fontnik, rend à 24 px avec une marge de quelques pixels autour du glyph) [Source : mapbox/sdf-glyph-foundry]. À très grand corps, le SDF finit par montrer ses limites (bords légèrement mous), et pour les idéogrammes CJK denses, la résolution SDF est un problème connu de qualité de rendu [Source : maplibre-gl-js #2990].
2.2 Le format : {fontstack}/{range}.pbf
La propriété racine glyphs d’un style est un template d’URL :
{
"glyphs": "https://fonts.example.com/{fontstack}/{range}.pbf"
}
Source : MapLibre Style Spec — Glyphs.
Deux tokens que le moteur remplit à la volée :
{fontstack}: la liste de polices demandée par une couche (voir 2.3).{range}: une plage de 256 codepoints Unicode —0-255,256-511,512-767… Le plan multilingue de base (BMP) fait 65 536 codepoints, soit 256 fichiers PBF au maximum par police [Source : MapLibre Style Spec — Glyphs].
Le moteur ne télécharge que les plages nécessaires au texte réellement affiché. Une carte de France en alphabet latin ne charge en pratique que 0-255 (Basic Latin + Latin-1) et parfois 256-511 (Latin étendu). C’est l’astuce qui rend le système viable : on n’envoie jamais toute une police au client.
text-font: ["Noto Sans Regular"] + label "Chamonix"
│
▼ le moteur détermine les codepoints → plage 0-255
GET https://fonts.example.com/Noto%20Sans%20Regular/0-255.pbf
│
▼ PBF gzippé (~quelques dizaines de Ko), mis en cache
256 glyphs SDF prêts pour le shader
2.3 text-font est le fontstack — et c’est une pile de secours
Dans une couche symbol, la propriété text-font est un tableau ordonné : une police primaire suivie de polices de secours. C’est ça, un « fontstack » [Source : Mapbox — Manage fontstacks].
{
"id": "place-city",
"type": "symbol",
"source-layer": "place",
"layout": {
"text-field": ["get", "name:latin"],
"text-font": ["Noto Sans Medium", "Noto Sans Regular"],
"text-size": 14
}
}
Le fallback fonctionne glyph par glyph, pas label par label : pour chaque caractère, le moteur prend le premier fontstack de la liste qui possède ce glyph. Un label mêlant latin et un caractère absent de la police primaire composera donc les deux polices sur le même mot. Côté serveur, la valeur du token {fontstack} devient la liste jointe par des virgules — et le serveur de glyphs doit savoir concaténer ces polices à la volée (c’est ce que fait le serveur d’OpenMapTiles) ou exposer un répertoire pour chaque combinaison exacte [Source : openmaptiles/fonts].
Piège majeur : le nom du fontstack doit correspondre exactement à un répertoire disponible sur le serveur de glyphs.
"Noto Sans Regular"et"Noto Sans"ne sont pas la même chose ; si le répertoire n’existe pas, la requête renvoie un 404 et les labels disparaissent silencieusement. C’est la cause n°1 de « ma carte n’a plus de texte » quand on change de fournisseur de tuiles sans changer le serveur de glyphs [Source : openmaptiles/fonts #7].
3. Quelles polices les fournisseurs utilisent vraiment
La réponse courte : du sans-serif, et pour l’open source, presque toujours du Noto. Le détail est plus instructif, parce qu’il oppose deux mondes — les fournisseurs propriétaires avec leur police maison, et l’écosystème vectoriel ouvert convergé sur Noto.
| Fournisseur | Police(s) des labels | Type de rendu | Réutilisable ? |
|---|---|---|---|
| Google Maps | Roboto (interface : Product Sans) | maison | Non (propriétaire) |
| Apple Maps | San Francisco (SF Pro) | maison | Non (propriétaire système Apple) |
| Mapbox Streets | DIN Pro + polices maison | vectoriel SDF | Non (licence commerciale) |
| MapTiler | Noto Sans, Roboto, Metropolis | vectoriel SDF | Oui (OFL/Apache) |
| OpenMapTiles (fonts) | Noto Sans, Open Sans, PT Sans, Roboto, Metropolis | vectoriel SDF | Oui (OFL/Apache) |
| Protomaps | Noto Sans (Regular/Medium/Italic) | vectoriel SDF | Oui (OFL) |
| OpenStreetMap Carto | Noto → DejaVu Sans → Hanazono → Unifont | raster | Oui (OFL/libre) |
Sources : DesignYourWay — Google Maps font, OpenStreetMap Wiki — CartoCSS/fonts, openmaptiles/fonts, protomaps/basemaps-assets, MapTiler — fonts and labels.
3.1 Pourquoi Noto domine l’open source
Noto est le projet de Google visant à couvrir tous les scripts Unicode — le nom vient de « no more tofu », le « tofu » étant le rectangle vide ▯ affiché quand un caractère n’a pas de glyph. Trois raisons en font le défaut de fait des cartes vectorielles libres :
- Couverture : une carte est mondiale par nature. Les labels OSM contiennent du latin, du cyrillique, du grec, de l’arabe, du CJK, du devanagari… Noto Sans couvre presque tout, ce qui minimise les tofus [Source : OpenStreetMap Wiki — CartoCSS/fonts].
- Cohérence : les différents scripts de Noto partagent des proportions communes, donc un label bilingue reste harmonieux.
- Licence OFL : la SIL Open Font License autorise l’embarquement, la modification et la redistribution, y compris commerciale. C’est ce qui permet à MapTiler, Protomaps et OpenMapTiles de la servir sans négociation [Source : openmaptiles/fonts].
MapTiler part d’un socle « Noto Sans » patché par Klokan Technologies, complété par Roboto et Metropolis pour les styles au caractère plus géométrique [Source : MapTiler — fonts and labels].
3.2 Le corollaire licence : on ne réutilise pas la police du voisin
Roboto (Google Maps, Apache 2.0) est réutilisable, mais Product Sans, DIN Pro et San Francisco ne le sont pas. San Francisco est réservé aux plateformes Apple par sa licence ; recréer « le look d’Apple Maps » ou « de Google Maps » avec leur police exacte sur sa propre carte est un problème juridique, pas technique. C’est une raison de plus pour laquelle l’écosystème ouvert s’est aligné sur Noto/Roboto : ce sont les seules polices de qualité mondiale librement embarquables dans un serveur de glyphs.
4. Bonnes pratiques de typographie cartographique
Le choix de la fonte SDF n’est que la plomberie. Le rendu final dépend surtout de comment on l’utilise. Les conventions ci-dessous sont largement partagées entre cartographes ; les sources faisant autorité sont le guide d’Axis Maps (cartographes) et le guide de design Mapbox.
4.1 Sans-serif par défaut à l’écran
Aucune étude ne montre une supériorité intrinsèque du serif ou du sans-serif en cartographie ; la règle est de choisir et de rester cohérent [Source : Axis Maps — Labeling]. Mais à l’écran, aux petits corps typiques d’une carte (10–14 px), le sans-serif l’emporte : les labels font un à trois mots, et les polices à grande hauteur d’x, contreformes ouvertes et sans empattements restent lisibles là où un serif se brouille [Source : Cartography Guide — Typography]. D’où le choix universel de Noto Sans, Roboto ou équivalents.
Les serifs et italiques gardent un usage sémantique classique (voir 4.3), pas décoratif.
4.2 Hiérarchie : donner un rôle à chaque axe typographique
Une carte lisible encode l’importance et la nature des objets dans la typographie. Les axes disponibles, du plus fort au plus subtil :
| Axe | Encode typiquement |
|---|---|
Taille (text-size) | Importance / rang (métropole vs village) |
Graisse (text-font) | Hiérarchie primaire vs secondaire |
Couleur (text-color) | Catégorie (routes, eau, POI) |
Casse (text-transform) | Rang administratif (pays en capitales) |
| Style (italique) | Nature physique (eau, relief) |
Interlettrage (text-letter-spacing) | Étendue spatiale (grandes régions espacées) |
La règle : un axe = une variable. Ne pas encoder l’importance à la fois par la taille et la couleur si ces deux dimensions doivent porter des informations différentes [Source : Axis Maps — Labeling].
4.3 L’eau en italique
Convention cartographique ancienne et quasi universelle : les hydronymes (mers, lacs, rivières) sont en italique, souvent dans un bleu, pour les distinguer immédiatement des toponymes terrestres. L’italique évoque le mouvement et sépare visuellement le réseau hydrographique du reste [Source : Axis Maps — Labeling]. C’est pour ça que les serveurs de glyphs livrent systématiquement une variante Italic : ce n’est pas décoratif, c’est une catégorie sémantique.
4.4 Capitales et interlettrage pour l’administratif
Les entités étendues (pays, régions, océans) sont souvent en capitales avec un fort interlettrage positif. L’espacement suggère l’étendue géographique et permet d’étaler le label sur toute la zone sans grossir le corps. Pour les labels primaires, un tracking de +20 à +30 (unités Mapbox, soit ~0,02–0,03 em) ouvre le texte sans le disloquer ; au-delà, l’effet devient illisible [Source : Guide to map design — Mapbox].
4.5 Halos : détacher le texte du fond sans le montrer
Le halo (contour clair autour du texte) est ce qui permet à un label de rester lisible par-dessus une forêt, une route ou une orthophoto. Les principes :
- Halo clair sur texte foncé, ou l’inverse. En général, halo blanc (1–2 px) autour d’un texte sombre.
- Discret : le but est de détacher le label, pas d’afficher un contour visible. On joue sur l’opacité (halo semi-transparent) pour éviter l’effet « autocollant » [Source : Guide to map design — Mapbox].
- Limite technique : dans MapLibre,
text-halo-widthest plafonné à 1/4 du corps de la police (text-size). Au-delà, le halo ne s’épaissit plus [Source : MapLibre Style Spec — text-halo-width].
4.6 Traduction en propriétés MapLibre
Toutes ces conventions se codent dans les propriétés text-* d’une couche symbol. Les principales [Source : MapLibre Style Spec — Symbol] :
| Propriété | Rôle | Convention |
|---|---|---|
text-font | Fontstack (police + secours) | Graisse = hiérarchie |
text-size | Corps en px | Rang / zoom |
text-color | Couleur du texte | Bleu pour l’eau |
text-halo-color / -width | Halo | Blanc, ≤ ¼ du corps |
text-transform | uppercase / lowercase | Capitales pour l’admin |
text-letter-spacing | Interlettrage (em) | Positif pour les grandes zones |
text-max-width | Retour à la ligne (em) | ~8 pour éviter les labels trop larges |
text-field | Attribut affiché | name:fr, name:latin… |
Exemple d’une couche « pays » appliquant capitales + interlettrage + halo :
{
"id": "place-country",
"type": "symbol",
"source-layer": "place",
"filter": ["==", ["get", "class"], "country"],
"layout": {
"text-field": ["get", "name:fr"],
"text-font": ["Noto Sans Bold"],
"text-transform": "uppercase",
"text-letter-spacing": 0.15,
"text-size": 14,
"text-max-width": 8
},
"paint": {
"text-color": "#334155",
"text-halo-color": "rgba(255,255,255,0.8)",
"text-halo-width": 1.5
}
}
5. Les compromis à arbitrer
C’est le cœur de la question. Choisir une police de carte, ce n’est pas choisir un font-family : c’est arbitrer entre couverture, poids réseau, nombre de fontstacks, licence et qualité de rendu.
5.1 Couverture linguistique vs poids
Plus une police couvre de scripts, plus il y a de fichiers PBF à générer et à héberger. Noto Sans complet, c’est jusqu’à 256 plages par graisse. En pratique le client ne télécharge que ce qu’il affiche, mais le CJK change l’échelle du problème : le texte chinois/japonais/coréen a une très mauvaise localité dans l’espace des codepoints — une seule tuile peut réclamer des dizaines de fichiers PBF différents, et le volume total est énorme [Source : Mapbox — Use locally generated ideographs].
Arbitrage : si la carte est régionale (Europe), servir une police latine + les scripts voisins suffit. Si elle est mondiale et affiche les noms locaux, il faut assumer le poids CJK — ou passer aux polices locales (5.3).
5.2 Nombre de fontstacks = coût d’hébergement et de requêtes
Chaque combinaison unique de text-font dans le style est un répertoire de PBF distinct à générer et héberger, et chaque fontstack supplémentaire visible sur une tuile est une requête HTTP de plus. Trop de graisses différentes multiplie les allers-retours réseau et, pour les cartes hors-ligne mobiles, le volume à embarquer [Source : Mapbox — Manage fontstacks].
Corollaire souvent ignoré : les polices variables ne sont pas supportées. Chaque graisse (Regular, Medium, Bold) et chaque style (Italic) est un fontstack séparé, cuit indépendamment. Une hiérarchie riche (Regular + Medium + Bold + Italic) = quatre jeux de PBF. La bonne pratique est de se limiter à 2–3 graisses réellement nécessaires.
5.3 Glyphs servis (SDF) vs polices locales
Il existe une échappatoire au poids du CJK : localIdeographFontFamily. Cette option de MapLibre GL JS (héritée de Mapbox) dit au moteur de générer localement les glyphs des plages « CJK Unified Ideographs » et « Hangul Syllables » à partir d’une police installée sur l’appareil, via TinySDF (l’API Canvas du navigateur), au lieu de les télécharger [Source : Mapbox — Use locally generated ideographs].
| Aspect | Glyphs servis (SDF PBF) | localIdeographFontFamily |
|---|---|---|
| Couverture | Tous scripts fournis | CJK + Hangul uniquement |
| Poids réseau CJK | Très lourd | Nul (police locale) |
| Rendu | Identique partout | Dépend de l’OS/navigateur |
| Contrôle typo | Total | Aucun (police système) |
| Latin, arabe, devanagari | Servis normalement | Toujours servis (non concernés) |
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.example.com/style.json",
// CJK rendu avec la police système, le reste servi en SDF
localIdeographFontFamily: "'Noto Sans CJK SC', 'Hiragino Sans', sans-serif",
});
Arbitrage clair : pour une carte grand public multilingue, localIdeographFontFamily économise énormément de bande passante au prix d’un rendu CJK non maîtrisé (variable selon l’appareil). Pour une carte où le rendu doit être pixel-identique partout (impression, capture), on sert tout en SDF et on assume le poids.
5.4 Licence
Résumé opérationnel : OFL et Apache 2.0 autorisent l’hébergement et la redistribution des glyphs, y compris en usage commercial — c’est le cas de Noto, Roboto, Open Sans, PT Sans, Metropolis, tous packagés par OpenMapTiles [Source : openmaptiles/fonts]. À l’inverse, les polices maison des grands (Product Sans, DIN Pro, San Francisco) sont hors de portée. Vérifier la licence avant de cuire une police en glyphs qu’on va servir publiquement : servir des PBF, c’est redistribuer la police sous forme dérivée.
5.5 Qualité de rendu SDF
Le SDF est cuit à 24 px : c’est excellent aux corps usuels d’une carte, correct en zoom, mais montre ses limites en très grand corps (titres) et, surtout, pour les glyphs CJK complexes dont les traits fins souffrent de la résolution SDF fixe [Source : maplibre-gl-js #2990]. Si le CJK est central et doit être net, localIdeographFontFamily (rendu vectoriel local) donne souvent un meilleur résultat que les PBF servis.
6. Générer et servir ses propres glyphs
Si l’on suit la logique de la série (tuiles auto-hébergées, sprite régénéré en CI), servir ses propres glyphs est le dernier maillon pour ne plus dépendre du serveur de fonts d’un fournisseur.
6.1 Les outils
| Outil | Techno | Notes |
|---|---|---|
| node-fontnik | Node + C++ | La référence historique ; utilisé par le repo OpenMapTiles |
| openmaptiles/fonts | Node | Empaquette Noto/Open Sans/PT Sans/Roboto/Metropolis, prêt à builder |
| MapLibre Font Maker | Web | UI : dépose un TTF/OTF, télécharge les fontstacks + ranges |
| build_pbf_glyphs | Rust | Plus rapide, sans dépendances C++ à compiler |
| versatiles-glyphs | Rust | Générateur SDF récent |
Toutes produisent la même chose : un répertoire par fontstack, contenant les fichiers {range}.pbf (0-255.pbf, 256-511.pbf…) [Source : build_pbf_glyphs].
6.2 Générer avec le repo OpenMapTiles
git clone https://github.com/openmaptiles/fonts
cd fonts
npm install
node ./generate.js
# → _output/Noto Sans Regular/0-255.pbf, 256-511.pbf, …
# _output/Noto Sans Bold/… etc.
Source : openmaptiles/fonts — README.
6.3 Servir en statique
Les PBF sont des fichiers statiques : n’importe quel serveur ou CDN fait l’affaire — exactement comme le sprite. Deux points de vigilance :
- gzip : les PBF de glyphs sont pré-gzippés par les générateurs. Servir avec le bon
Content-Encoding: gzip, ou dé-gzipper, sinon le moteur ne les lit pas [Source : openmaptiles/fonts]. - CORS : la carte tourne dans un navigateur sur un autre domaine que le serveur de glyphs → il faut
Access-Control-Allow-Origin.
Le style pointe alors :
{
"glyphs": "https://fonts.mondomaine.fr/{fontstack}/{range}.pbf"
}
C’est aussi un candidat idéal pour un pipeline CI (comme le sprite Pinhead de l’article dédié) : on cuit les glyphs une fois, on les pousse sur le CDN, on ne les touche plus.
7. Pièges connus
7.1 Le fontstack introuvable (404 silencieux)
Le plus fréquent. Le text-font du style référence "Metropolis Regular" mais le serveur de glyphs n’expose que "Noto Sans Regular" → 404, labels absents, aucune erreur visible sur la carte. Toujours vérifier que chaque valeur de text-font du style a un répertoire correspondant sur le serveur de glyphs. C’est le point qui casse quand on migre un style MapTiler vers un serveur de fonts maison [Source : openmaptiles/fonts #7].
7.2 Les espaces dans le nom du fontstack
Les noms contiennent des espaces (Noto Sans Regular). Dans l’URL, ils sont encodés (Noto%20Sans%20Regular). Un serveur mal configuré qui ne décode pas correctement renverra un 404 sur un répertoire qui existe pourtant.
7.3 Le fallback ne remplace pas une police absente du serveur
Le fallback de text-font opère par glyph manquant dans la police, pas par fontstack absent du serveur. Si la police primaire existe sur le serveur mais n’a pas le caractère, MapLibre passe à la suivante. Mais si le fontstack lui-même renvoie 404, il n’y a pas de repli automatique vers une autre entrée — le label ne s’affiche simplement pas. Le fallback n’est donc pas un filet de sécurité contre les erreurs d’hébergement.
7.4 Le texte de droite à gauche (RTL)
L’arabe et l’hébreu ont besoin du plugin mapbox-gl-rtl-text chargé explicitement (maplibregl.setRTLTextPlugin(...)), sinon le texte s’affiche à l’envers. Ce n’est pas une question de police mais de moteur de mise en forme — mais ça touche directement le rendu des labels dans ces scripts.
7.5 Les scripts non supportés par MapLibre (devanagari, etc.)
Certains scripts complexes (devanagari, birman…) demandent un réordonnancement et des ligatures que le moteur de shaping de MapLibre ne fait pas nativement. Protomaps contourne ça avec les Positioned Glyph Fonts (PGF) : les tuiles exportent le nom déjà positionné (pgf:name:*), et une fontstack custom mappe les codepoints sur des glyphs pré-positionnés [Source : Devanagari in the Protomaps Basemap — oliverwipfli.ch]. À connaître si une carte doit afficher correctement ces scripts.
8. Recommandation
Pour une carte vectorielle auto-hébergée dans l’esprit de cette série :
- Police : Noto Sans (OFL), servie en glyphs SDF. C’est le défaut de fait, pour la couverture et la licence. Ajouter une variante géométrique (Metropolis, Roboto) seulement si le style le justifie.
- Graisses : se limiter à 2–3 fontstacks (Regular + Medium/Bold + un Italic pour l’eau). Chaque fontstack en plus = hébergement + requêtes.
- Typographie : sans-serif partout, hiérarchie par taille et graisse, eau en italique bleu, admin en capitales espacées, halo blanc discret (≤ ¼ du corps).
- CJK : si la carte est mondiale et le CJK secondaire, activer
localIdeographFontFamilypour éviter le poids réseau ; si le rendu CJK doit être maîtrisé, servir en SDF et assumer. - Hébergement : cuire les glyphs une fois (OpenMapTiles fonts ou
build_pbf_glyphs), pousser sur un CDN en gzip + CORS, et pointerglyphsdessus dans le style — le même schéma que le sprite.
La police d’une carte se change en une ligne (text-font + glyphs). Ce qui coûte, c’est la couverture, le nombre de fontstacks et la qualité CJK — les trois seuls compromis qui méritent réflexion.