Un résultat exprimé en cellules
Le service hexodos calcule sur un réseau découpé en cellules H3 : un itinéraire est la suite des cellules traversées, une isochrone l'ensemble des cellules atteintes, chacune avec son temps de trajet. Les réponses donnent l'index de chaque cellule : c'est lui qui permet de joindre un résultat à une table d'indicateurs (population, commerces, équipements…) sans aucun calcul géométrique.
| Fonction | Méthode | Usage |
|---|---|---|
route | POST | Itinéraire entre deux points : durée, distance, cellules traversées dans l'ordre |
isochrone | POST | Cellules accessibles en un ou plusieurs temps donnés, avec le temps de chacune |
status | GET | Modes servis, emprise et date des données |
Service expérimental. hexodos couvre pour l'instant l'Île-de-France et n'est pas ouvert sur tous les serveurs. Là où il ne l'est pas, ses fonctions répondent 503 avec le code service_not_configured (voir erreurs).
Chaque fonction s'appelle avec la clé d'API habituelle (en-tête X-API-Key). Les requêtes route et isochrone sont des objets JSON envoyés en POST ; une autre méthode reçoit 405. Les noms des champs sont en anglais, les messages d'erreur en français.
Qu'est-ce qu'une cellule H3 ?
H3 est un découpage de la Terre entière en hexagones, emboîtés par niveaux de finesse appelés résolutions. Chaque cellule porte un index unique et stable, écrit en texte (8b1fb4662475fff), qui ne dépend d'aucune frontière administrative. hexodos travaille à la résolution 11, des hexagones d'environ 25 m de côté et 2 150 m² : deux résultats, ou un résultat et un indicateur, se comparent donc par simple égalité d'index.
La bibliothèque H3 existe pour la plupart des langages (h3 en Python, h3-js en JavaScript, des extensions pour PostgreSQL, DuckDB ou BigQuery) : elle retrouve la cellule d'un point, le contour d'une cellule ou sa cellule parente à une résolution plus grossière.
Points et modes de déplacement
Un point de départ ou d'arrivée s'écrit de l'une des deux façons suivantes, jamais les deux à la fois :
| Forme | Exemple |
|---|---|
| Coordonnées | {"lat": 48.855199, "lon": 2.354035} |
| Cellule de résolution 11 | {"cell": "8b1fb4662475fff"} |
Le calcul part du point de rattachement de la cellule : le point du réseau le plus proche de son centre. Si la cellule demandée n'en a pas (un parc, un bâtiment), hexodos retient la cellule voisine la plus proche qui en a un, jusqu'à environ 500 m. La réponse indique la cellule réellement utilisée et la distance entre le point demandé et son rattachement (snap_distance_m).
mode | Mode | Vitesse |
|---|---|---|
pedestrian | Piéton | 5 km/h, sens uniques ignorés |
bicycle | Vélo | 16 km/h sur revêtement lisse ou pavé, moins sur chemin compacté, terre ou gravier ; 1 s par feu ; sens uniques respectés sauf contresens cyclables |
auto | Voiture | Vitesse maximale autorisée de la voie, sinon une vitesse par type de route ; 130 km/h au plus ; 2 s par feu, jusqu'à 2 s par carrefour franchi sans feu, selon la priorité ; interdictions de tourner respectées |
Les noms des modes sont ceux du calcul d'itinéraires (costing). Il n'y a pas de réglage par mode : pas d'options de profil, pas de trafic, pas d'heure de départ.
Calculer un itinéraire
Renvoie la durée et la distance du trajet le plus rapide entre deux points, la suite des cellules traversées et le polygone de leur fusion, un ruban d'une cinquantaine de mètres de large.
Paramètres
| Champ | Description |
|---|---|
originrequis | Point de départ : coordonnées ou cellule (voir points). |
destinationrequis | Point d'arrivée, sous la même forme. |
moderequis | pedestrian, bicycle ou auto. |
geometry | false pour ne pas recevoir le polygone (par défaut true) : réponse plus légère si seules les cellules vous intéressent. |
Un champ inconnu est refusé (400) : une faute de frappe n'est jamais prise pour une valeur par défaut.
Exemple
curl "https://geoservice.example.com/v2/hexodos/route" \
-H "X-API-Key: $GEOSERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": {"lat": 48.855199, "lon": 2.354035},
"destination": {"lat": 48.853138, "lon": 2.349185},
"mode": "pedestrian"
}'
const res = await fetch("https://geoservice.example.com/v2/hexodos/route", {
method: "POST",
headers: { "X-API-Key": GEOSERVICE_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
origin: { lat: 48.855199, lon: 2.354035 },
destination: { lat: 48.853138, lon: 2.349185 },
mode: "pedestrian",
}),
});
const { duration_s, distance_m, cells } = await res.json();
import httpx
r = httpx.post(
"https://geoservice.example.com/v2/hexodos/route",
headers={"X-API-Key": GEOSERVICE_KEY},
json={
"origin": {"lat": 48.855199, "lon": 2.354035},
"destination": {"lat": 48.853138, "lon": 2.349185},
"mode": "pedestrian",
},
)
route = r.json()
print(route["duration_s"], route["distance_m"], len(route["cells"]))
{
"mode": "pedestrian",
"resolution": 11,
"origin": {"cell": "8b1fb4662475fff", "snap_distance_m": 6.6},
"destination": {"cell": "8b1fb46624a4fff", "snap_distance_m": 30.7},
"duration_s": 430.3,
"distance_m": 597.4,
"cells": [
"8b1fb4662475fff", "8b1fb4662474fff", "8b1fb4662429fff", "8b1fb466242dfff",
"8b1fb4662428fff", "8b1fb466242cfff", "8b1fb4662421fff", "8b1fb4662425fff",
"8b1fb4662424fff", "8b1fb4662420fff", "8b1fb4662426fff", "8b1fb4662422fff",
"8b1fb4662435fff", "8b1fb4662422fff", "8b1fb4662435fff", "8b1fb4662431fff",
"8b1fb4662430fff", "8b1fb4662433fff", "8b1fb4662432fff", "8b1fb4662414fff",
"8b1fb4662589fff", "8b1fb466258bfff", "8b1fb46624a4fff"
],
"geometry": {
"type": "Polygon",
"coordinates": [[[2.354605692753111, 48.85510856402714], …]]
}
}
Réponse
| Champ | Contenu |
|---|---|
resolution | Résolution des cellules de la réponse : toujours 11. |
origin, destination | Cellule réellement utilisée et distance, en mètres, entre le point demandé et son rattachement. |
duration_s, distance_m | Durée en secondes et distance en mètres, rattachements compris. |
cells | Cellules traversées, dans l'ordre du trajet. Un trajet qui sort d'une cellule puis y revient la cite deux fois : c'est le cas ci-dessus de 8b1fb4662422fff et 8b1fb4662435fff. |
geometry | Polygone GeoJSON (longitude, latitude) de la fusion des cellules traversées ; il peut avoir un trou quand le trajet fait une boucle. Son type est Polygon en pratique, mais peut être MultiPolygon : lisez type avant coordinates. |
Départ et arrivée dans la même cellule donnent une durée et une distance nulles, et une seule cellule.
Calculer une isochrone
Renvoie, pour chaque durée demandée, les cellules accessibles depuis un point avec le temps de trajet de chacune, et le multipolygone de leur fusion.
Paramètres
| Champ | Description |
|---|---|
originrequis | Point de départ : coordonnées ou cellule. |
moderequis | pedestrian, bicycle ou auto. |
contours_minrequis | Liste de durées en minutes, strictement positives, entières ou non : [5, 10, 15]. |
closing | Taille de la fermeture, entier de 0 à 3 (par défaut 2). 0 rend les seules cellules atteintes par le réseau. |
compact | true pour recevoir les cellules compactées, sans les temps (par défaut false). |
geometry | false pour ne pas recevoir les multipolygones (par défaut true). |
Exemple
curl "https://geoservice.example.com/v2/hexodos/isochrone" \
-H "X-API-Key: $GEOSERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": {"lat": 48.855199, "lon": 2.354035},
"mode": "pedestrian",
"contours_min": [5, 10]
}'
const res = await fetch("https://geoservice.example.com/v2/hexodos/isochrone", {
method: "POST",
headers: { "X-API-Key": GEOSERVICE_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
origin: { lat: 48.855199, lon: 2.354035 },
mode: "pedestrian",
contours_min: [5, 10],
}),
});
const { contours } = await res.json();
for (const c of contours) console.log(c.minutes, c.cell_count, c.filled_count);
import httpx
r = httpx.post(
"https://geoservice.example.com/v2/hexodos/isochrone",
headers={"X-API-Key": GEOSERVICE_KEY},
json={
"origin": {"lat": 48.855199, "lon": 2.354035},
"mode": "pedestrian",
"contours_min": [5, 10],
},
)
for contour in r.json()["contours"]:
print(contour["minutes"], contour["cell_count"], contour["filled_count"])
{
"mode": "pedestrian",
"resolution": 11,
"closing": 2,
"origin": {"cell": "8b1fb4662475fff", "snap_distance_m": 6.6},
"contours": [
{
"minutes": 5,
"cell_count": 189,
"filled_count": 34,
"cells": [
{"cell": "8b1fb4662475fff", "duration_s": 0.0},
{"cell": "8b1fb4662474fff", "duration_s": 6.1},
{"cell": "8b1fb466240dfff", "duration_s": 6.1, "filled": true},
{"cell": "8b1fb4662471fff", "duration_s": 22.0},
…
],
"geometry": {
"type": "MultiPolygon",
"coordinates": [[[[2.3525623500053947, 48.85769664414983], …]]]
}
},
{"minutes": 10, "cell_count": 810, "filled_count": 103, "cells": […], "geometry": {…}}
]
}
Réponse
| Champ | Contenu |
|---|---|
closing | Taille de fermeture appliquée : celle demandée, 2 sinon. |
contours | Un contour par durée, dans l'ordre demandé. Chaque contour contient les cellules des contours plus courts : celui de 10 minutes reprend les 189 cellules de celui de 5 minutes. |
cell_count | Nombre de cellules du contour, cellules ajoutées par la fermeture comprises. Les cellules atteintes par le réseau sont cell_count − filled_count. |
filled_count | Nombre de cellules ajoutées par la fermeture ; 0 avec "closing": 0. |
cells | Cellules par temps croissant, l'origine en tête à 0 s. Une cellule ajoutée par la fermeture porte "filled": true ; une cellule atteinte n'a pas ce champ (il ne vaut jamais false). |
geometry | Multipolygone GeoJSON (longitude, latitude) de la fusion des cellules du contour, trous restants compris. |
La fermeture des isochrones
Le réseau ne traverse pas toutes les cellules : les cœurs d'îlots, les cours et les jardins sans allée n'en contiennent aucune voie. Les cellules réellement atteintes forment donc une zone criblée de trous. La fermeture les bouche : elle ajoute à chaque contour les cellules enclavées dans des vides étroits, sans jamais retirer une cellule atteinte.
"closing": 0 · 707 cellules, 32 trous"closing": 2 · 810 cellules, dont 103 ajoutéesfilled)
10 minutes à pied depuis l'Hôtel de Ville, Paris. Tracé à partir des réponses réelles de isochrone.
| Sujet | Règle |
|---|---|
| Ce qu'elle bouche | Les vides plus étroits qu'environ deux fois sa taille, en cellules, quelle que soit leur surface : un îlot, une cour en longueur, une échancrure étroite du bord. Avec la taille 2, par défaut, les vides d'environ 170 m. |
| Ce qu'elle laisse | Les vides plus larges : un fleuve, un faisceau de voies ferrées, un grand parc ou un cimetière ne sont que grignotés sur leurs bords. À vélo, 10 minutes depuis le même point : 473 trous sans fermeture, 2 avec. |
| Taille | closing de 0 à 3. Plus elle est grande, plus les vides bouchés sont larges. 0 rend le résultat brut, les seules cellules atteintes. |
| Temps d'une cellule ajoutée | Celui de la cellule atteinte la plus rapide autour d'elle (à une distance au plus égale à closing). À temps égal, une cellule atteinte est listée avant une cellule ajoutée. |
| D'un contour à l'autre | Chaque contour est fermé séparément, et un contour fermé contient toujours les contours plus courts fermés. Une cellule ajoutée dans un contour court peut être atteinte dans un contour plus long : elle y figure sans filled, avec son propre temps. |
| Itinéraires | Jamais fermés. |
Fermée ou brute ? La fermeture donne une zone continue, proche de ce qu'on dessinerait à la main, et compte les cellules d'un îlot entouré de rues accessibles. Pour ne garder que les cellules qu'une voie traverse réellement, filtrez les cellules marquées filled, ou demandez "closing": 0.
Cellules compactées
Avec "compact": true, une cellule parente remplace ses sept filles quand toutes sont dans le contour : la liste mêle plusieurs résolutions, triée par index, sans temps ni marque filled. Les 810 cellules du contour de 10 minutes ci-dessus tiennent alors en 144 index. uncompactCells de la bibliothèque H3, à la résolution 11, redonne les cellules du contour fermé ; la géométrie ne change pas.
Joindre une isochrone à des indicateurs
Une table d'indicateurs indexée par cellule H3 de résolution 11 se joint à une isochrone par égalité d'index. Exemple : le nombre de commerces accessibles en 10 minutes à pied, à partir d'un fichier commerces.csv à deux colonnes, cell et commerces.
import httpx
import pandas as pd
r = httpx.post(
"https://geoservice.example.com/v2/hexodos/isochrone",
headers={"X-API-Key": GEOSERVICE_KEY},
json={
"origin": {"lat": 48.855199, "lon": 2.354035},
"mode": "pedestrian",
"contours_min": [10],
"geometry": False,
},
)
cellules = pd.DataFrame(r.json()["contours"][0]["cells"]) # cell, duration_s, filled
commerces = pd.read_csv("commerces.csv") # cell, commerces
zone = cellules.merge(commerces, on="cell", how="left").fillna({"commerces": 0})
print(zone["commerces"].sum(), "commerces à 10 minutes à pied")
# Sans les cellules ajoutées par la fermeture (colonne absente s'il n'y en a aucune) :
atteintes = zone[zone["filled"].isna()] if "filled" in zone else zone
print(atteintes["commerces"].sum(), "dans les cellules qu'une voie traverse")
const res = await fetch("https://geoservice.example.com/v2/hexodos/isochrone", {
method: "POST",
headers: { "X-API-Key": GEOSERVICE_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
origin: { lat: 48.855199, lon: 2.354035 },
mode: "pedestrian",
contours_min: [10],
geometry: false,
}),
});
const { contours } = await res.json();
// commerces : Map index de cellule → nombre, chargée depuis votre table
const total = contours[0].cells.reduce((somme, c) => somme + (commerces.get(c.cell) ?? 0), 0);
console.log(`${total} commerces à 10 minutes à pied`);
Indicateurs à une autre résolution
Une table établie à une résolution plus grossière, par exemple 9 (cellules d'environ 175 m de côté), se joint en ramenant chaque cellule de l'isochrone à sa parente : h3.cell_to_parent(cell, 9) en Python, cellToParent(cell, 9) avec h3-js. Plusieurs cellules de l'isochrone partagent alors la même parente : pour un indicateur de stock (population, nombre de commerces), répartissez la valeur de la parente entre ses filles, ou ne gardez que les parentes entièrement couvertes, plutôt que de la compter plusieurs fois.
Avec "compact": true, décompactez d'abord la liste à la résolution 11 (uncompact_cells(cells, 11) en Python, uncompactCells(cells, 11) avec h3-js) : les index compactés ne sont pas de résolution 11 et ne joindraient pas.
État de l'index
Indique les modes servis, l'emprise couverte et la date des données OpenStreetMap utilisées.
{
"status": "ok",
"index": {
"format_version": 2,
"resolution": 11,
"modes": {
"pedestrian": {"cells": 1912703, "nodes": 4651628, "transitions": 11230804, "penalty_coefficient": 0.5, "max_speed_kmh": 5.0},
"bicycle": {"cells": 1838717, "nodes": 4092629, "transitions": 8977218, "penalty_coefficient": 0.5, "max_speed_kmh": 16.0},
"auto": {"cells": 1101982, "nodes": 2314993, "transitions": 4118005, "penalty_coefficient": 0.25, "max_speed_kmh": 130.0}
},
"source": {
"name": "ile-de-france-latest.osm.pbf",
"sha256": "efb85f303f80c124ac9892ca19573ae0f8c238cccc69e4ff3dd1f85e2163658e",
"osm_timestamp": "2026-09-16T20:21:21Z"
},
"bbox": [1.4261447, 48.1063114, 3.5617685, 49.2439152],
"built_at": "2026-10-08T11:50:10+00:00"
}
}
| Champ | Contenu |
|---|---|
index.modes | Modes servis, avec le nombre de cellules couvertes et la vitesse maximale de chacun. |
index.bbox | Emprise : longitude et latitude minimales, puis maximales. Un point hors de l'emprise n'a aucun réseau à proximité. |
index.source | Fichier OpenStreetMap d'origine et date de ses données (osm_timestamp). |
index.built_at | Date de construction de l'index. |
Ce que le service ne fait pas
| Sujet | Ce qu'il en est |
|---|---|
| Tracé sur les rues | Un itinéraire est une suite de cellules et le ruban de leur fusion, pas une ligne qui suit les rues. Pour un tracé, utilisez le calcul d'itinéraires. |
| Guidage | Aucune instruction de navigation, aucun nom de rue. |
| Transport en commun | Non pris en charge : piéton, vélo et voiture seulement. |
| Trajets très courts | Départ et arrivée sont ramenés au rattachement de leur cellule, en général à quelques dizaines de mètres (plus loin, jusqu'à environ 500 m, quand la cellule n'a pas de réseau : voir snap_distance_m) : sous 2 minutes, l'écart dû au rattachement domine et la durée n'est pas précise. |
| Trafic et horaires | Pas de trafic, pas d'heure de départ : les vitesses sont fixes. |
| Feux, virages, pentes | Le temps perdu aux feux et aux carrefours est forfaitaire, sans cycle ni sens du virage. En voiture, les interdictions de tourner sont respectées, sauf les rares qui passent par une voie entière ; à vélo, elles ne sont pas prises en compte. Pas de dénivelé. |
| Matrices | Pas de matrice de temps : utilisez sources_to_targets. |
| Emprise | Île-de-France seulement (voir status). |
Une requête qui produirait une réponse trop grande, par exemple une isochrone voiture de longue durée, est refusée (422 limit_exceeded) plutôt que tronquée. Une isochrone fermée compte plus de cellules qu'une isochrone brute : si le refus est dû à la seule fermeture, le message l'indique, et la même requête passe avec "closing": 0.
Erreurs
Les erreurs d'hexodos sont au même format que celles du GeoService (application/problem+json, voir Erreurs) : testez le membre code. Leur instance est le chemin de la fonction dans le moteur (/isochrone), et l'en-tête X-GGO-Upstream vaut hexodos:….
{
"type": "tag:galigeo.com,2026:geoservice:no_network_nearby",
"title": "Aucun réseau à proximité",
"status": 422,
"detail": "Aucun réseau du mode demandé à proximité de l'origine (environ 500 m).",
"instance": "/isochrone",
"code": "no_network_nearby"
}
| HTTP | code | Cause | Que faire |
|---|---|---|---|
| 400 | invalid_parameter | Corps illisible, champ absent, mal formé ou inconnu, coordonnées et cellule à la fois, mode inconnu, durée ou distance au-delà de la limite. Le detail nomme le champ en cause : « Champ 'contours_min' obligatoire. » | Corriger la requête. |
| 413 | payload_too_large | Corps de requête trop volumineux | Une requête tient en quelques centaines d'octets : vérifier ce qui est envoyé. |
| 422 | no_network_nearby | Aucun réseau du mode à environ 500 m du point, par exemple hors de l'emprise | Rapprocher le point d'une voie, ou changer de mode. |
| 422 | route_impossible | Départ et arrivée non reliés par le réseau du mode (île, zone fermée) | Changer de point ou de mode. |
| 422 | limit_exceeded | Réponse trop grande, détectée pendant le calcul | Réduire la durée ou le nombre de contours, ou demander "closing": 0 si le message le propose. |
| 500 | internal_error | Erreur inattendue du moteur | Prévenir Galigeo. |
| 503 | index_unavailable | Données du moteur momentanément indisponibles | Réessayer après un délai. |
| 503 | service_not_configured | hexodos n'est pas ouvert sur ce serveur (erreur du GeoService) | Ne pas réessayer : contacter Galigeo. |