Documentation
Tout ce qu'il faut pour router sur des données OpenStreetMap depuis Python : le modèle de graphe, l'extraction, les moteurs de routage et le serveur REST.
Installation
pip install py-osrm-backend
Deux dépendances sont installées : flask pour le serveur REST et osmium pour les fichiers PBF. Les fichiers XML n'ont besoin que de la bibliothèque standard. Le paquet s'importe sous le nom osrm.
terminaldev
# Depuis les sources, avec pytest, build et twine git clone https://github.com/GalTechDev/py-osrm-backend cd py-osrm-backend pip install -e ".[dev]"
Architecture
Le paquet reprend le découpage d'OSRM : un extractor transforme la carte en graphe, un engine répond aux requêtes, un server les expose en HTTP. Graph, GraphBuilder et DijkstraEngine sont ré-exportés à la racine.
| Module | Contenu |
|---|---|
| osrm.structures.graph | Node, Edge, Graph, haversine_distance |
| osrm.extractor.graph_builder | GraphBuilder |
| osrm.extractor.parser | OSMParser (XML) |
| osrm.extractor.pbf_parser | OSMPBFParser (PBF, osmium) |
| osrm.engine.dijkstra | DijkstraEngine |
| osrm.engine.contraction_hierarchies | ContractionHierarchies, CHNode, Shortcut |
| osrm.server.api | app Flask, init_app() |
Le graphe
Un graphe orienté stocké sous forme de liste d'adjacence. Les nœuds sont indexés par leur identifiant OSM ; chaque Edge porte un poids (en kilomètres quand il vient d'une carte) et le nom de la rue. Une rue à double sens correspond simplement à deux arêtes.
codeexample.py
from osrm import Graph from osrm.structures.graph import haversine_distance g = Graph() g.add_node(1, 48.8566, 2.3522) # Paris g.add_node(2, 45.7640, 4.8357, {"name": "Lyon"}) d = haversine_distance(48.8566, 2.3522, 45.7640, 4.8357) # ~392 km g.add_edge(1, 2, d, name="A6") g.add_edge(2, 1, d, name="A6") # sens inverse len(g) # 2 g.get_edges(1) # [Edge(source=1, target=2, ...)]
| add_node(id, lat, lon, tags=None) | Ajoute (ou remplace) un nœud et crée sa liste d'adjacence vide. |
| add_edge(source, target, weight, name="") | Ajoute une arête orientée. Lève ValueError si l'un des nœuds est absent. |
| get_edges(node_id) | Arêtes sortantes d'un nœud (liste vide s'il est inconnu). |
| get_node(node_id) | Le Node, ou None. |
| nodes / adj_list | Les dictionnaires sous-jacents : {id: Node} et {id: [Edge]}. |
| haversine_distance(lat1, lon1, lat2, lon2) | Distance orthodromique en kilomètres (rayon terrestre 6371 km). |
Extraction OSM
GraphBuilder.build_graph(file_path) lit une carte et renvoie un Graph prêt à router. Les fichiers terminant par .pbf passent par osmium ; tout le reste est lu comme de l'OSM XML.
- 1. Chaque nœud est ajouté au graphe avec ses coordonnées et ses tags.
- 2.
Seules les voies portant un tag
highwaysont conservées (routes, chemins, trottoirs…). - 3.
Chaque paire de nœuds consécutifs devient une arête pondérée par la distance haversine et nommée d'après le tag
namede la voie. - 4.
Une arête inverse est ajoutée sauf si la voie a
oneway=yes. Les segments qui référencent des nœuds hors de l'extrait sont ignorés.
codeexample.py
from osrm import GraphBuilder from osrm.extractor.parser import OSMParser # Pipeline complet graph = GraphBuilder().build_graph("new-caledonia.osm.pbf") # Ou les parsers seuls, pour vos propres filtres parser = OSMParser() nodes = parser.parse_nodes("map.osm") # {id: Node} ways = parser.parse_ways("map.osm") # [{"id", "nodes", "tags"}] bus = [w for w in ways if w["tags"].get("bus") == "yes"]
OSMParser (XML) et OSMPBFParser (PBF) partagent la même interface : parse_nodes(path) et parse_ways(path). Le parser XML lit le fichier en flux avec iterparse et libère chaque élément une fois lu.
Dijkstra
Le moteur par défaut, et celui qu'utilise le serveur REST. Aucun pré-traitement : créez-le et interrogez-le tout de suite. La recherche s'arrête dès que la destination est atteinte.
codesignature
DijkstraEngine(graph: Graph) engine.shortest_path( start_node_id: int, end_node_id: int, ) -> Tuple[float, List[int]]
| Cas | Résultat |
|---|---|
| Chemin trouvé | (distance, [départ, …, arrivée]) |
| Aucun chemin | (inf, []) |
| Nœud inconnu | ValueError |
codeexample.py
import math from osrm import DijkstraEngine engine = DijkstraEngine(graph) distance, path = engine.shortest_path(start_id, end_id) if math.isinf(distance): print("Aucun itinéraire") else: coords = [(graph.nodes[n].lat, graph.nodes[n].lon) for n in path]
Contraction Hierarchies
La technique d'accélération sur laquelle repose OSRM. preprocess() contracte les nœuds du moins au plus important et ajoute un raccourci pour chaque chemin qui passait par eux. query() lance ensuite une recherche bidirectionnelle qui ne fait que monter dans la hiérarchie.
codeexample.py
from osrm.engine.contraction_hierarchies import ContractionHierarchies ch = ContractionHierarchies(graph) ch.preprocess() # une fois distance, path = ch.query(start_id, end_id) print(len(ch.shortcuts), "raccourcis")
| preprocess(max_nodes=None) | Calcule l'ordre de contraction (différence d'arêtes + voisins contractés, mis à jour paresseusement) et construit les listes d'arêtes montantes/descendantes. max_nodes limite le travail, pour les tests. |
| query(start, end) | Renvoie (distance, path) comme Dijkstra, ou (inf, []). Lève ValueError pour un nœud inconnu. |
| ch_nodes | {id: CHNode(id, level, contracted)} : le rang de chaque nœud dans la hiérarchie. |
| shortcuts | Liste de Shortcut(source, target, weight, via), où via est le nœud contracté. |
Le Graph fourni n'est jamais modifié : les raccourcis vivent dans l'objet ContractionHierarchies, le même graphe peut donc toujours alimenter un DijkstraEngine. Le chemin renvoyé est déplié et ne contient que des nœuds d'origine. Une witness search (limitée à WITNESS_SEARCH_LIMIT = 500 nœuds visités) évite les raccourcis inutiles.
Lancer le serveur
osrm.server.api contient une app Flask au niveau du module. Appelez init_app(path) une fois pour construire le graphe et le moteur Dijkstra, puis lancez l'app comme n'importe quelle application Flask.
coderun_server.py
from osrm.server.api import app, init_app init_app("data/new-caledonia.osm.pbf") # FileNotFoundError si absent app.run(host="0.0.0.0", port=5000)
Ou directement en ligne de commande, configuré par variables d'environnement :
OSRM_FILE=map.osm.pbf python -m osrm.server.api
| Variable | Défaut | Description |
|---|---|---|
| OSRM_FILE | data.osm | Carte à charger (.osm ou .osm.pbf). |
| OSRM_HOST | 127.0.0.1 | Adresse d'écoute (0.0.0.0 pour l'exposer). |
| OSRM_PORT | 5000 | Port d'écoute. |
| FLASK_DEBUG | — | 1 active le mode debug de Flask. |
GET /health renvoie {"status": "ok", "nodes": N} une fois le graphe chargé, ou 503 avant.
Endpoint route
/route/v1/driving/{lon},{lat};{lon},{lat}[;…]
Même forme d'URL qu'OSRM : longitude en premier, coordonnées séparées par ;. Deux points de passage ou plus sont acceptés ; chacun est accroché au nœud le plus proche relié à au moins une arête, et les tronçons sont enchaînés en un seul itinéraire.
terminal200 OK
$ curl "localhost:5000/route/v1/driving/166.44,-22.27;166.46,-22.29;166.47,-22.30" { "code": "Ok", "routes": [{ "distance": 4870.2, "duration": 352.9, "geometry": { "type": "LineString", "coordinates": [[166.4401, -22.2703], ...] } }] }
| Champ | Description |
|---|---|
| distance | Longueur totale en mètres, comme OSRM. |
| duration | Temps estimé en secondes, à 50 km/h constants (AVERAGE_SPEED_MPS = 13,8 m/s) : il n'y a pas encore de profil de vitesse. |
| geometry | LineString GeoJSON de paires [lon, lat], prête pour Leaflet ou MapLibre. |
Codes d'erreur
| 400 InvalidRequest | Moins de deux coordonnées, ou une coordonnée qui n'est pas au format lon,lat. |
| 400 NoSegment | Aucun nœud routable n'a été trouvé pour une coordonnée. |
| 404 NoRoute | Deux points de passage consécutifs ne sont pas reliés dans le graphe. |
| 500 Error | Exception inattendue ; message contient son texte. |
Chaque corps d'erreur a la forme {"code": …, "message": …}.
Docker
Le dépôt fournit un Dockerfile et un docker-compose.yml. Placez votre carte dans data/map.osm (ou changez OSRM_FILE) et démarrez le service : l'API écoute sur le port 5000 et le healthcheck du conteneur appelle /health.
docker compose up -d
# Mode dev : sources montées, debug Flask, port 5001
docker compose --profile dev up py-osrm-backend-dev
Tests
La suite couvre les structures de graphe, l'extracteur XML, les deux moteurs et l'API multi-points. Elle tourne avec unittest comme avec pytest.
python -m unittest discover -s tests -v
# ou
pytest
Limites
py-osrm-backend privilégie la lisibilité à la vitesse brute. Bon à savoir avant de charger un pays entier :
- chevron_right Le graphe entier vit en mémoire sous forme d'objets Python.
- chevron_right
Pas de profil de véhicule : toute voie
highwayest utilisable, les poids sont des distances et seuloneway=yesest compris. - chevron_right Le serveur accroche les coordonnées par un parcours linéaire de tous les nœuds, et route toujours avec Dijkstra.
- chevron_right Le pré-traitement CH tourne en Python pur avec un ordre de contraction simple : il reste lent sur de gros extraits, idéal pour apprendre et pour des cartes régionales.