Reconstruire Google Maps chez soi : ce que ça m’a appris
J’ai toujours eu un truc avec les cartes. Il y a quelque chose de paradoxal là-dedans : c’est à la fois une abstraction totale de la réalité (des formes, des couleurs, des lignes) et en même temps une invitation permanente à l’exploration. Je me surprends encore à zoomer sur un coin paumé juste en me demandant “mais il y a quoi, là ?”
Un jour je me suis mis en tête de faire ma propre carte, juste pour comprendre ce qu’il y a derrière ces tuiles qu’on affiche sans jamais se poser la question. Alors oui, Google Maps c’est une prouesse d’ingénierie à l’échelle de la planète, rien à voir avec ce qu’on va faire ici. Mais on peut quand même se faire sa propre carte, chez soi, et c’est hyper cool.

Le vrai problème : une sphère sur du plat
Une carte, c’est censé représenter des trucs qui sont sur une sphère (la terre, désolé les platistes) sur un truc plat : un écran, une feuille de papier. Sauf que ça ne colle pas naturellement. Essaie de peler une orange et de poser la peau bien à plat sur la table : ça se déchire, ça se froisse, ça s’étire. C’est exactement ce problème-là, en cartographie, qui s’appelle une projection.

Il en existe plein, chacune avec ses compromis : certaines gardent les surfaces justes, d’autres les angles, d’autres les distances. Aucune ne garde tout en même temps. Donc la vraie question, c’est : laquelle choisir, et sur quel critère ?
Pourquoi Mercator
Réponse : ça dépend de ce que tu veux faire après. Et ce qu’on veut faire, c’est découper le monde en petites cases régulières, qu’on peut re-subdiviser à l’infini. Il se trouve qu’il y a une projection qui fait exactement ça : Mercator. Elle est conforme (elle garde les angles localement) et elle projette toute la Terre sur un carré. Un carré, ça se subdivise en carrés plus petits, encore et encore, proprement. Une projection comme Albers ou Robinson, elle, donne des formes irrégulières impossibles à quadriller correctement.
C’est pour ça que Google Maps (et à peu près tout le monde) utilise Mercator. Ce n’est pas la plus fidèle géographiquement : elle explose la taille des régions polaires, le Groenland a l’air presque aussi grand que l’Afrique alors que l’Afrique fait 14 fois sa taille. Mais elle a la seule propriété qui compte ici : un quadrillage propre.

Bon, on a un carré. Pourquoi le découper ?
Le découpage en tuiles
Parce qu’une image de toute la Terre en haute résolution, ça pèse des téraoctets. Impossible à télécharger, impossible à faire tenir en mémoire dans un navigateur. Vu qu’on a déjà un carré bien net grâce à Mercator, la solution est presque évidente : on le découpe en petits carrés, et on ne charge que ceux qu’on regarde. Ça s’appelle des tuiles.
Chaque tuile a une adresse z/x/y : z le niveau de zoom, x/y sa position dans la grille à ce niveau. Zoom 0 = 1 tuile pour toute la planète. Zoom 1 = 4 tuiles. Zoom 2 = 16. Ça double à chaque niveau, un quadtree tout bête. Le navigateur ne charge jamais “la carte”, juste les tuiles visibles à l’écran.
Et comme la grille est fixe, deux personnes qui regardent Rennes au même zoom demandent exactement la même tuile donc cacheable direct (CDN, cache HTTP).
Reste à savoir ce qu’on met dedans. Deux écoles : le raster, des images toutes cuites générées côté serveur, c’est simple, mais si tu veux changer une couleur faut tout régénérer. Le vectoriel : pas un pixel, juste des géométries brutes (points, lignes, polygones) que le navigateur dessine lui-même au vol. Résultat, style et données sont découplés, tu changes tout côté client sans toucher au serveur. C’est ce qu’on utilisera.

Il faut des données
Pour remplir des tuiles il faut des données géographiques. Là-dessus, OpenStreetMap : le Wikipedia de la carto, une base mondiale alimentée par des contributeurs, réutilisable librement (licence ODbL, faut juste créditer “© OpenStreetMap contributors”). La qualité varie selon les zones, mais sur l’essentiel (routes, bâtiments, POI) c’est largement suffisant.
Petit détail qui va compter juste après : les données OSM sont en latitude/longitude (WGS84, coordonnée sur la “sphère”), donc pas encore projetées. Il va falloir leur appliquer la projection Mercator qu’on vient de choisir.
La stack, et le code
Trois briques pour aller du fichier OSM à l’écran :
- PostGIS pour stocker et requêter spatialement (une extension spatiale de PostgreSQL)
- Martin pour transformer une requête spatiale en tuile vectorielle à la demande
- MapLibre GL JS pour peindre tout ça dans le navigateur

Pour la démo, on va charger la plus belle région du monde : la Bretagne. Oui, c’est totalement objectif 😁 Geofabrik donne un extrait OSM de la Bretagne en .osm.pbf => 300 Mo, largement suffisant pour jouer (contre 32 Go pour toute l’Europe).
Un petit compose.yml monte PostGIS et Martin (docker compose up -d) :
services:
postgis:
image: postgis/postgis:17-3.5-alpine
platform: linux/amd64
container_name: postgis
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: password
POSTGRES_DB: carto
ports:
- "5432:5432"
volumes:
- postgis-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d carto"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
martin:
image: maplibre/martin:v0.8.0
platform: linux/amd64
container_name: martin
depends_on:
postgis:
condition: service_healthy
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://admin:password@postgis:5432/carto
volumes:
postgis-data:
Puis osm2pgsql importe le fichier dans PostGIS (3 mins) via lacommande :
osm2pgsql --create --slim -U admin -H localhost -W -d carto bretagne-latest.osm.pbf
PS : Après l’import, il faudra relancer martin pour le scan des tables (docker compose down puis down compose up -d de nouveau).
Il range tout dans quatre tables par type de géométrie : planet_osm_point (arrêts de bus, cafés), planet_osm_line (sentiers, cours d’eau), planet_osm_roads (routes principales), planet_osm_polygon (bâtiments, parcs, plans d’eau). Une ligne de planet_osm_point :
osm_id | highway | name | public_transport | way
1425715786 | bus_stop | Le Rheu (L'Autre Lieu) | platform | POINT (-200025.75 6123334.68)
La colonne way, c’est la géométrie et elle est déjà en Web Mercator (SRID 3857, vérifiable avec ST_SRID(way)). C’est osm2pgsql qui a fait la projection au moment de l’import. Boucle fermée : la théorie de tout à l’heure devient une opération concrète, une seule fois, à l’écriture. Après ça, plus personne n’a besoin d’y repenser.
Martin, lui, s’occupe de transformer ces lignes en tuiles MVT (Mapbox Vector Tiles, du Protobuf, un format binaire compact). Au démarrage il inspecte geometry_columns pour découvrir les tables, et expose chacune à http://localhost:3000/nom_table/{z}/{x}/{y}. Quand une tuile est demandée, il exécute en gros ça :
SELECT ST_AsMVT(tile, 'planet_osm_point', 4096, 'geom')
FROM (
SELECT
ST_AsMVTGeom(
way,
ST_TileEnvelope(:z, :x, :y),
4096, -- extent : résolution interne de la tuile
64, -- buffer : marge en pixels pour éviter les artefacts aux bords
true -- clip_geom
) AS geom,
osm_id, highway, name
FROM planet_osm_point
WHERE way && ST_TileEnvelope(:z, :x, :y)
) AS tile;
ST_TileEnvelope calcule la zone couverte par la tuile demandée, l’index GIST sur way (créé par osm2pgsql) filtre en quelques millisecondes même sur des millions de lignes, ST_AsMVTGeom reprojette dans le repère local de la tuile, ST_AsMVT empile tout dans un blob protobuf. Martin ne fait aucun calcul géométrique lui-même, il délègue tout à PostGIS. Un petit cache LRU en mémoire évite de regénérer une tuile déjà servie récemment.
Reste à peindre. MapLibre prend les tuiles vectorielles et les dessine en WebGL, chaque géométrie devient des triangles envoyés au GPU, ce qui permet d’afficher des dizaines de milliers d’objets sans ramer, et ouvre la porte aux bâtiments 3D, au pitch/bearing, au relief.
<div id="map" style="height: 100vh;"></div>
<script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
<script>
const map = new maplibregl.Map({
container: 'map',
center: [-1.68333, 48.11], // Rennes
zoom: 13,
style: {
version: 8,
sources: {
osm: {
type: 'vector',
url: 'http://localhost:3000/planet_osm_polygon,planet_osm_line',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
},
layers: [
{ id: 'bg', type: 'background', paint: { 'background-color': '#eef1f2' } },
{
id: 'water', type: 'fill', source: 'osm', 'source-layer': 'planet_osm_polygon',
filter: ['==', 'natural', 'water'],
minzoom: 4,
paint: { 'fill-color': '#9fd3e8' }
},
{
id: 'green', type: 'fill', source: 'osm', 'source-layer': 'planet_osm_polygon',
filter: ['any', ['==', 'leisure', 'park'], ['==', 'landuse', 'grass'], ['==', 'natural', 'wood']],
minzoom: 9,
paint: { 'fill-color': '#b9e0a5', 'fill-opacity': 0.9 }
},
{
id: 'region-boundary', type: 'line', source: 'osm', 'source-layer': 'planet_osm_polygon',
filter: ['==', 'boundary', 'administrative'],
minzoom: 6,
paint: { 'line-color': '#b0b0b0', 'line-width': 1, 'line-dasharray': [3, 2] }
},
{
id: 'buildings', type: 'fill', source: 'osm', 'source-layer': 'planet_osm_polygon',
filter: ['has', 'building'], minzoom: 14,
paint: { 'fill-color': '#dcd6cc' }
},
{
id: 'roads', type: 'line', source: 'osm', 'source-layer': 'planet_osm_line',
filter: ['all', ['has', 'highway'], ['!in', 'highway', 'motorway', 'trunk']],
minzoom: 12,
paint: {
'line-color': '#ffffff',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 0.5, 18, 5]
}
},
{
id: 'motorways', type: 'line', source: 'osm', 'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'motorway', 'trunk'],
minzoom: 5,
paint: {
'line-color': '#ff9d42',
'line-width': ['interpolate', ['linear'], ['zoom'], 5, 1, 15, 5]
}
}
]
}
});
</script>
Quatre trucs à retenir : une source peut regrouper plusieurs tables, chaque layer choisit la sienne via source-layer. filter c’est ton WHERE, mais exécuté côté client sur des données déjà en mémoire, sans aller-retour réseau. minzoom gère le niveau de détail, pas juste une histoire de perf, un bâtiment affiché au zoom 5 c’est juste du bruit visuel. Et l’ordre des layers, c’est la profondeur : MapLibre peint du fond vers le premier plan, d’où l’eau et la végétation avant les bâtiments et les routes.

Ce que ça m’a appris
Le truc qui reste, plus que la stack elle-même : PostGIS ne connaît que des géométries et des attributs. Martin ne connaît que des requêtes SQL et un format binaire. MapLibre ne connaît que des règles de style appliquées à des couches. Aucune brique ne sait à quoi “ma carte” doit ressembler et c’est exactement ce qui rend le système flexible : je change le style sans toucher à la donnée, ou l’inverse.
Ce n’est pas propre à la cartographie, c’est le même principe qu’un modèle et sa vue, une scène 3D et son moteur de rendu. Dès qu’une couche se met à connaître les détails de celle qui la consomme, le système devient rigide.
Le plus surprenant, ce n’était pas d’afficher une carte. C’était de réaliser que tout repose sur trois idées simples : une projection choisie pour une propriété précise, un découpage qui en découle, et un moteur qui peint le résultat. Individuellement, rien de magique. C’est la composition qui donne l’impression de magie.

J’ai poussé le style complet (bâtiments 3D, labels, POI) sur un repo GitHub :