API d'itinéraires
Cinq points de terminaison sur un réseau routier couvrant tout le Canada : un itinéraire entre deux points, une isochrone autour d'un point (la zone que vous pouvez atteindre depuis ce point dans un temps donné, retournée sous forme de polygone), une matrice de temps de parcours entre plusieurs points, l'accrochage de trace, qui colle une trace GPS au réseau routier, et l'optimisation de l'ordre des arrêts, qui ordonne un ensemble d'arrêts et trace l'itinéraire qui les relie. Les cinq sont calculés par Valhalla, le moteur d'itinéraires libre.
Chaque requête nécessite une clé API; voir Authentification.
Les coordonnées sont toujours en lng,lat, la longitude d'abord, en degrés. Le réseau routier ne
couvre que le Canada, et c'est la chose la plus utile à savoir avant de commencer. La section « Couverture
et cas d'échec » plus bas précise exactement quelles requêtes cela exclut.
Voici un vrai itinéraire au centre-ville de Calgary, tracé à partir de la réponse affichée à côté :
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.03, 51.07],
zoom: 11,
});
const route = await unmap.router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: "auto" });
unmap.map!.on("load", () => {
unmap.map!.addSource("route", {
type: "geojson",
data: { type: "Feature", properties: {}, geometry: route.geometry },
});
unmap.map!.addLayer({
id: "route-line",
type: "line",
source: "route",
layout: { "line-cap": "round", "line-join": "round" },
paint: { "line-color": "#FF3E9A", "line-width": 3 },
});
});@unmap/routing est le client autonome (sans dépendance à la carte); @unmap/sdk expose le même
objet sous unmap.router. Tout ce qui suit est le contrat HTTP sous les deux.
Route
La couverture se limite au Canada, et certaines paires ne peuvent pas être calculées. Voir Couverture et cas d'échec avant de bâtir autour d'un itinéraire qui peut échouer.
GET /route
from(obligatoire) :lng,lat, l'origine.to(obligatoire) :lng,lat, la destination. Il n'y a pas de point de passage intermédiaire. Une requête porte exactement deux points, et un troisième nombre séparé par une virgule donne un400, pas une étape.mode(facultatif,autopar défaut) :auto,car,bicycle,pedestrian,truckoutransit.caretautodésignent le même profil automobile;carexiste parce queautose lit comme « automatique » pour qui ne connaît pas Valhalla. Toute autre valeur donne un400:{ "error": "mode must be one of auto, car, bicycle, pedestrian, truck, transit" }. Jusqu'au 2026-09-03, une valeur non reconnue retombait silencieusement surauto; ce n'est plus le cas.depart_at(mode=transitseulement) : une date et heure ISO 8601. Une valeur avec un décalage ouZdésigne un instant; sans décalage, elle utilise le fuseau horaire du flux de transport. Ce paramètre est obligatoire pour le transport en commun et refusé pour tout autre mode. Il n'existe aucune optionarrive_by.- Profil de camion (facultatif,
mode=truckseulement) :height,width,lengthen mètres,weightetaxle_loaden tonnes,axle_counten nombre entier,hazmatàtrueoufalse, etuse_truck_routede0à1. Voir « Routage camion » plus bas pour les valeurs par défaut, les plages et ce que les données couvrent ou non. L'un de ces paramètres sur un autre mode quetruckdonne un400. avoid(facultatif) : une liste séparée par des virgules parmitolls,highways,ferries. Une préférence, pas une exclusion : lisez « Évitement » plus bas avant de vous y fier.tollsethighwaysne valent que pour les modes de conduite;ferriesvaut pour tous les modes routiers. Le transport en commun refuse les trois. Une valeur inconnue, une valeur vide, ou une valeur que le mode ne connaît pas donne un400.avoid=highwaysetuse_highwayssont le même réglage et ne peuvent pas être envoyés ensemble.use_highways(facultatif, modes de conduite seulement) : de0à1. Disposition à emprunter les autoroutes et les routes nationales.0les évite,1les privilégie. Omise, Valhalla utilise sa propre valeur par défaut (0,5). Accepté surauto,carettruck. Surbicycleoupedestrian, c'est un400:{ "error": "use_highways applies only to mode=auto, car, or truck" }. C'est l'option de coût déjà présente dans Valhalla sur le graphe Canada, pas un second moteur. Voir « Routage industriel » plus bas.use_hills(facultatif, vélo et marche seulement) : de0à1. Disposition à monter.0évite les côtes même si le trajet s'allonge,1y est indifférent. Omise, Valhalla utilise sa propre valeur par défaut (0,25surbicycle,0,5surpedestrian). Accepté surbicycleetpedestrian. Surauto,caroutruck, c'est un400:{ "error": "use_hills applies only to mode=bicycle or pedestrian" }. Voir « Dénivelé » plus bas pour savoir pourquoi la conduite est exclue.
curl "https://api.unmap.dev/route?from=-114.0719,51.0447&to=-113.9871,51.0899&mode=auto" \
-H "Authorization: Bearer $UNMAP_API_KEY"import { Router } from "@unmap/routing";
const router = new Router({ key: "um_live_..." });
const route = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: "auto" });{
"distanceMeters": 15627,
"durationSeconds": 991,
"geometry": {
"type": "LineString",
"coordinates": [[-114.071903, 51.044666], [-114.072726, 51.044691], /* … */]
}
}distanceMeters: la distance routière, arrondie au mètre.durationSeconds: le temps de parcours estimé, arrondi à la seconde.geometry: uneLineStringGeoJSON ordinaire de paires[lng, lat]à six décimales, soit environ 0,1 m. La première et la dernière coordonnée sont accrochées au tronçon routier le plus proche : elles se situent donc près des points demandés plutôt qu'exactement dessus. Pour un point situé sur une route, l'écart reste sous 10 m ; pour une adresse, sous 50 m environ.warnings(présent uniquement s'il y a quelque chose à signaler, sur tous les modes) : avertissements au niveau de l'itinéraire. Branchez surcode, qui est stable ; le texte peut être reformulé.
Lisez warnings avant de vous fier à une extrémité. S'il n'y a aucune route près du point
demandé, le moteur ne refuse pas la requête. Il déplace le point vers la route la plus proche
qu'il peut emprunter et renvoie un itinéraire convaincant vers un autre endroit. Interrogé entre
deux localités qu'aucune route ne relie, il a répondu 200 avec un itinéraire de 10,1 km après
avoir déplacé la destination de 292,3 km, et dans une province dense un écart de 19 km a été
mesuré. Vous obtenez tout de même un itinéraire, car il est généralement correct et s'arrête
simplement avant la fin, mais ENDPOINT_RELOCATED vous indique qu'il n'atteint pas le lieu
demandé :
{
"distanceMeters": 10142,
"durationSeconds": 913,
"geometry": { "type": "LineString", "coordinates": [/* … */] },
"warnings": [
{
"code": "ENDPOINT_RELOCATED",
"where": "destination",
"message": "The route's destination is 292 km from the point requested: no road the vehicle can use was found nearer, so the engine moved it."
}
]
}code | Signification | Que faire |
|---|---|---|
ENDPOINT_RELOCATED | L'itinéraire ne part pas du point envoyé ou n'y arrive pas. where précise l'extrémité et le message porte la distance | Considérez l'extrémité comme non atteinte. C'est ce à quoi ressemblent un site de forage, un bail éloigné ou un nouveau lotissement |
ENDPOINT_RELOCATION_NOT_CHECKED | L'itinéraire est revenu sans tracé exploitable : la vérification n'a pas pu s'exécuter | Ne l'interprétez pas comme « les extrémités sont correctes » |
CLOSURE_IN_FORCE_NEAR_ROUTE | Une autorité provinciale a fermé une route à moins de 250 m du tracé, en ce moment | Attendez-vous à un itinéraire erroné. Le graphe est reconstruit chaque semaine : il ne peut pas contourner une fermeture |
CLOSURE_SCHEDULED_NEAR_ROUTE | Même chose, mais à venir. effectiveFrom indique la date | Rien, pour un trajet aujourd'hui. Utile à signaler pour un trajet en préparation |
Ce n'est pas propre aux camions, et le déplacement d'extrémité se produit plus souvent sur
mode=auto que sur mode=truck, car le coût automobile atteint des fragments de réseau
inaccessibles à un camion. Sur mode=truck, les mêmes constatations figurent aussi dans
truck.validation, qui est un rapport de conformité autonome ; warnings reste l'endroit à
consulter quel que soit le mode.
La couverture des fermetures porte sur l'Ontario, l'Alberta et la Colombie-Britannique, les seules provinces disposant de flux officiels. Les instantanés sont rafraîchis toutes les cinq minutes par la passerelle, jamais pendant votre requête : une panne chez un flux provincial ne devient donc pas une panne ici. Ailleurs, l'absence d'avertissement signifie qu'aucune vérification n'a eu lieu, et non qu'aucune route n'est fermée. Une fermeture est signalée comme étant PRÈS de votre itinéraire et non sur celui-ci : la plupart sont publiées sous forme d'un seul point, et la proximité est donc la seule affirmation honnête.
Les messages de warnings sont en anglais pour l'instant.
La passerelle ne renvoie pas le format interne de Valhalla tel quel. Elle le convertit dans cette forme plate afin que votre code ne dépende pas du contrat interne de Valhalla. La réponse ne contient aucune instruction de navigation virage par virage : la passerelle demande à Valhalla de ne pas la générer.
La polyligne n'est pas simplifiée, donc un long trajet est volumineux. Calgary vers Halifax revient à 4 867 062 m répartis sur 34 283 coordonnées. Simplifiez côté client si vous ne dessinez qu'une vue d'ensemble.
Erreurs. Un from ou un to absent, qui ne compte pas exactement deux valeurs séparées par
une virgule, ou dont une partie n'est pas un nombre fini, donne un 400 :
{ "error": "from and to must be lng,lat" }. Une moitié vide, comme dans from=,, donne aussi
un 400.
Une paire que Valhalla ne peut pas relier donne un 422 avec { "error": "no route" }. Toute
autre défaillance en amont donne un 502 avec le même corps; branchez donc sur le code de statut,
pas sur le message.
Isochrone
La couverture se limite au Canada, et certaines paires ne peuvent pas être calculées. Voir Couverture et cas d'échec avant de bâtir autour d'un itinéraire qui peut échouer.
GET /isochrone
lon,lat(obligatoires) : le point central, en deux paramètres distincts ici, plutôt que la paire uniquelng,latque prend/route.minutes(obligatoire) : une liste de bandes de temps séparées par des virgules, par exemple5,10,15. Les valeurs fractionnaires sont acceptées. Au plus quatre bandes, chacune de 120 minutes ou moins; voir les limites plus bas.mode(facultatif,autopar défaut) :auto,car,bicycle,pedestrianoutruck, validé exactement comme sur/route, et acceptant les mêmes paramètres de profil de camion.avoid,use_highways,use_hills(facultatifs) : les mêmes préférences de coût que/route, validées de la même façon. Une bande tracée avecavoid=ferriesest la zone atteignable sans traversier.
curl "https://api.unmap.dev/isochrone?lon=-114.0719&lat=51.0447&minutes=10,20&mode=pedestrian" \
-H "Authorization: Bearer $UNMAP_API_KEY"const bands = await router.isochrone([-114.0719, 51.0447], {
minutes: [10, 20],
mode: "pedestrian",
});import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 12,
});
const bands = await unmap.router.isochrone([-114.0719, 51.0447], { minutes: [10, 20], mode: "pedestrian" });
unmap.map!.on("load", () => {
unmap.map!.addSource("bands", { type: "geojson", data: bands });
unmap.map!.addLayer({
id: "bands-fill",
type: "fill",
source: "bands",
paint: { "fill-color": "#FF3E9A", "fill-opacity": 0.18 },
});
});Retourne une FeatureCollection GeoJSON, un objet Polygon ou MultiPolygon par bande demandée,
transmis tel quel depuis Valhalla. Valhalla renvoie déjà exactement la forme attendue ici, il n'y a
donc rien à traduire.
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"contour": 20,
"metric": "time",
"color": "#bf4040",
"opacity": 0.33,
"fill": "#bf4040",
"fillColor": "#bf4040",
"fillOpacity": 0.33,
"fill-opacity": 0.33
},
"geometry": { "type": "Polygon", "coordinates": [/* … */] }
}
]
}contour: la bande à laquelle appartient le polygone, dans l'unité demandée. Faites la correspondance avec votre requête sur cette valeur, pas sur la position dans le tableau.metric: toujourstimeici. La passerelle ne demande que des contours de temps, jamais de distance.color,opacity,fill,fillColor,fillOpacity,fill-opacity: le style suggéré par Valhalla, dans les différentes graphies qu'attendent les bibliothèques cartographiques. C'est un dégradé du vert au rouge choisi par Valhalla, et non par unmap; vous pouvez tout ignorer et colorer seloncontour.
Les entités arrivent de la plus grande bande à la plus petite. C'est déjà le bon ordre de dessin pour des polygones remplis : tracez-les dans l'ordre reçu et les petites bandes se posent sur les grandes.
// La collection s'ajoute directement à MapLibre, stylée depuis les propriétés de Valhalla.
map.addSource('bands', { type: 'geojson', data: bands })
map.addLayer({
id: 'bands-fill',
type: 'fill',
source: 'bands',
paint: { 'fill-color': ['get', 'color'], 'fill-opacity': ['get', 'fillOpacity'] },
})Limites. Au plus quatre contours par requête, et aucun contour de plus de 120 minutes. Ce sont
les limites de service de Valhalla lui-même, vérifiées à la passerelle pour qu'elles reviennent en
400 avec la règle nommée plutôt qu'en défaillance en amont :
- Plus de quatre valeurs dans
minutes:{ "error": "at most 4 minutes contours" }. - Toute valeur au-dessus de 120 :
{ "error": "minutes must be 120 or less" }.
Chaque contour est un calcul de coût distinct dans le conteneur d'itinéraires, ce qui explique aussi le plafond sur le nombre : une liste sans borne laisserait une seule requête faire un travail déraisonnable.
Erreurs. Les valeurs de minutes qui ne sont pas des nombres finis, ou qui sont nulles ou
négatives, sont écartées en silence plutôt que rejetées : minutes=10,abc,-5,20 retourne les
bandes de 10 et 20 minutes sans rien dire des deux valeurs abandonnées. Seule une liste vide
après ce filtrage est une erreur. Ce cas, ainsi qu'un lon ou un lat absent ou non numérique,
donne un 400 : { "error": "lon, lat and minutes required" }.
Un lon=&lat= vide donne aussi un 400. Toute défaillance en amont donne un 502 :
{ "error": "isochrone failed" }.
Matrix
La couverture se limite au Canada, et certaines paires ne peuvent pas être calculées. Voir Couverture et cas d'échec avant de bâtir autour d'un itinéraire qui peut échouer.
En service sur api.unmap.dev. Ce point de terminaison est derrière un indicateur de
fonctionnalité propre à chaque déploiement, activé ici, donc il répond normalement. Sur un
déploiement où l'indicateur n'est pas activé, il répond 404, indiscernable d'un chemin qui
n'existe pas du tout : un 404 signifie donc non activé, et non une faute de frappe dans votre
URL.
POST /matrix
origins(obligatoire) : un tableau de paires[lng, lat], jusqu'à 10.destinations(obligatoire) : un tableau de paires[lng, lat], jusqu'à 10.mode(facultatif,autopar défaut) : les mêmes cinq modes que/route.- Profil de camion (facultatif,
mode=truckseulement) : les mêmes champs que/route. Voir « Routage camion » plus bas. avoid(facultatif) : la même liste que/route, sous forme de tableau JSON :["tolls", "ferries"].use_highways(facultatif, modes de conduite seulement) : la même préférence de 0 à 1 que/route.use_hills(facultatif, vélo et marche seulement) : la même préférence de 0 à 1 que/route.
Les coordonnées voyagent dans le corps JSON plutôt que dans la chaîne de requête; la clé voyage de
la même façon que sur les deux points de terminaison ci-dessus, comme en-tête Authorization,
X-API-Key, ou ?key=.
curl -X POST "https://api.unmap.dev/matrix" \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"origins":[[-114.0719,51.0447]],"destinations":[[-113.9871,51.0899],[-114.1,51.05]],"mode":"auto"}'const res = await fetch('https://api.unmap.dev/matrix', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
origins: [[-114.0719, 51.0447]],
destinations: [[-113.9871, 51.0899], [-114.1, 51.05]],
mode: 'auto',
}),
});
const times = await res.json();const origins: [number, number][] = [[-114.0719, 51.0447]];
const destinations: [number, number][] = [[-113.9871, 51.0899], [-114.1, 51.05]];
const times = await unmap.router.matrix(origins, destinations)
// Les options viennent après les deux tableaux, comme pour route() et isochrone() :
const withTruck = await unmap.router.matrix(origins, destinations, {
mode: "truck",
truck: { height: 4.2, weight: 36 },
});// 200
{
"durations": [[612, 984]],
"distances": [[7823, 15627]]
}durations: secondes. Rangée par rangée : une ligne par origine, une colonne par destination, dans l'ordre où vous les avez envoyées.distances: mètres, même forme.- Un
0est une vraie réponse : c'est ce que vous obtenez sur la diagonale d'une requête où une origine est aussi une destination, où la réponse honnête est zéro seconde et zéro mètre, pas « inaccessible ». Vérifié en production : la diagonale d'une matrice Calgary/Banff retourne bien0, jamaisnull. - Si une seule paire origine-destination est inaccessible, ou dépasse la limite de distance
routière propre au moteur, la requête entière échoue, et une cellule
nulln'apparaît jamais en pratique. Ceci corrige une version antérieure de cette page, qui affirmait qu'une cellulenullsignifiait « inaccessible ». En sondant l'API déployée, on a découvert que Valhalla fait échouer toute la requêtesources_to_targetsde la même façon qu'il fait échouer un/routeinvalide, et non paire par paire. La limite de distance routière observée se situe quelque part entre environ 212 km et 300 km de route (Calgary vers Lethbridge à 212 km réussit, Calgary vers Edmonton à environ 300 km échoue) ; le chiffre exact est lemax_matrix_distancepropre à Valhalla, que ce déploiement ne configure pas et n'a pas encore lu. Le type(number | null)[][]et la gestion denulldans le SDK sont conservés : ils ne coûtent rien, sont corrects pour un vrai0, et restent corrects si une future version du moteur se met à mettrenullsur des cellules plutôt que de faire échouer la requête entière.
Limites. Au plus 10 origines et 10 destinations. Dépasser l'une ou l'autre donne un 400 :
{ "error": "at most 10 origins", "code": "too_many_locations" } (ou l'équivalent nommant les
destinations). Ces deux chiffres sont délibérément prudents : personne n'a encore mesuré la
limite de service propre à Valhalla pour ce point de terminaison sur ce déploiement, donc 10 par
10 est un plancher choisi pour garantir que le 400 de la passerelle survienne avant toute limite
en amont, pas une affirmation sur le vrai plafond.
Erreurs. Un origins ou destinations absent ou malformé donne un 400 :
{ "error": "origins and destinations must be arrays of lng,lat pairs", "code": "invalid_location" }.
Un corps qui n'est pas du JSON, qui n'est pas un objet JSON, ou qui dépasse 64 000 octets donne un
400 avec code: "invalid_body", avant même que vos coordonnées soient lues. Une paire
inaccessible ou au-delà de la limite de distance donne un 422 avec
{ "error": "no matrix: a pair is unreachable or beyond the engine's distance limit", "code": "no_route" }.
Toute autre défaillance en amont donne un 502 avec code: "routing_failed".
Cas d'usage. Un répartiteur qui choisit lequel de plusieurs chauffeurs envoyer à un nouveau travail : un seul appel, avec le travail comme unique destination et la position actuelle de chaque chauffeur comme origine, retourne le vrai temps de trajet de chacun en un aller-retour, prêt à trier par durée.
Match
La couverture se limite au Canada, et certaines paires ne peuvent pas être calculées. Voir Couverture et cas d'échec avant de bâtir autour d'un itinéraire qui peut échouer.
En service sur api.unmap.dev. Comme /matrix, ce point de terminaison a son propre indicateur
de fonctionnalité, activé ici. Là où il ne l'est pas, le point de terminaison répond 404, ce qui
signifie non activé et non une mauvaise URL.
POST /match
coordinates(obligatoire) : une trace GPS, tableau de paires[lng, lat], dans l'ordre enregistré. Au moins 2, au plus 100.mode(facultatif,autopar défaut) : les mêmes cinq modes que/route.- Profil de camion (facultatif,
mode=truckseulement) : les mêmes champs que/route. avoid(facultatif) : la même liste que/route, sous forme de tableau JSON :["tolls", "ferries"].use_highways(facultatif, modes de conduite seulement) : la même préférence de 0 à 1 que/route.use_hills(facultatif, vélo et marche seulement) : la même préférence de 0 à 1 que/route.
curl -X POST "https://api.unmap.dev/match" \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"coordinates":[[-114.0719,51.0447],[-114.0705,51.0451],[-114.069,51.0458]],"mode":"auto"}'const res = await fetch('https://api.unmap.dev/match', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
coordinates: [[-114.0719, 51.0447], [-114.0705, 51.0451], [-114.069, 51.0458]],
mode: 'auto',
}),
});
const cleaned = await res.json();const coordinates: [number, number][] = [
[-114.0719, 51.0447],
[-114.0705, 51.0451],
[-114.069, 51.0458],
];
const cleaned = await unmap.router.match(coordinates)// 200
{
"distanceMeters": 412,
"durationSeconds": 58,
"geometry": {
"type": "LineString",
"coordinates": [[-114.071903, 51.044666], [-114.070812, 51.045104], [-114.069318, 51.045802]]
}
}Retourne la même forme que /route : une trace appariée est un itinéraire. La géométrie porte
généralement plus de points que vous n'en avez envoyé, parce qu'elle suit le réseau routier entre
vos points enregistrés plutôt que de les relier par des lignes droites.
Limites. Au moins 2 et au plus 100 coordonnées. Moins de 2 donne un 400 :
{ "error": "coordinates must be at least 2 lng,lat pairs", "code": "invalid_location" }. Plus de
100 donne un 400 : { "error": "at most 100 coordinates", "code": "too_many_locations" }.
Comme les plafonds de la matrice, 100 est un plancher prudent plutôt qu'un plafond mesuré.
Erreurs. Un corps qui n'est pas du JSON, qui n'est pas un objet JSON, ou qui dépasse 64 000
octets donne un 400 avec code: "invalid_body". Comme /route et /matrix, une trace que le
moteur a comprise mais n'a pas pu apparier à une route donne un 422 avec
{ "error": "no match: the trace could not be snapped to the road network", "code": "no_route" }
(confirmé sur la passerelle déployée : une trace en pleine eau dans la baie d'Hudson renvoie
un 422). Toute autre défaillance en amont donne toujours un 502 avec
code: "routing_failed".
Cas d'usage. Nettoyer la trace GPS brute d'une camionnette de livraison avant de facturer un
client pour la distance parcourue : envoyez les points enregistrés à /match et récupérez la
vraie distance routière, plutôt que la somme de lignes droites, plus bruitée, que donnerait un
journal GPS brut.
Optimize
La couverture se limite au Canada, et certaines paires ne peuvent pas être calculées. Voir Couverture et cas d'échec avant de bâtir autour d'un itinéraire qui peut échouer.
Déploiement en cours. Comme /matrix et /match, ce point de terminaison est derrière son propre
indicateur de fonctionnalité et répond 404 tant que le fondateur ne l'active pas pour un
déploiement. Un 404 ici signifie pas encore disponible, pas une mauvaise URL.
POST /optimized-route
À partir d'un ensemble d'arrêts, retourne l'ordre dans lequel les visiter, plus l'itinéraire qui les relie. Le premier arrêt est le point de départ fixe et le dernier le point d'arrivée fixe; seuls les arrêts entre les deux sont réordonnés. C'est une contrainte réelle du moteur sous-jacent, pas une option que cette API retient : il n'y a pas d'option « aller-retour » ou « itinéraire ouvert ».
stops(obligatoire) : un tableau de paires[lng, lat]. Au moins 3, au plus 10.mode(facultatif,autopar défaut) : les mêmes cinq modes que/route.- Profil de camion (facultatif,
mode=truckseulement) : les mêmes champs que/route. avoid(facultatif) : la même liste que/route, sous forme de tableau JSON :["tolls", "ferries"].use_highways(facultatif, modes de conduite seulement) : la même préférence de 0 à 1 que/route.use_hills(facultatif, vélo et marche seulement) : la même préférence de 0 à 1 que/route.
curl -X POST "https://api.unmap.dev/optimized-route" \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"stops":[[-114.07,51.05],[-113.98,51.04],[-114.12,51.03],[-114.04,51.08]],"mode":"auto"}'const res = await fetch('https://api.unmap.dev/optimized-route', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
stops: [[-114.07, 51.05], [-113.98, 51.04], [-114.12, 51.03], [-114.04, 51.08]],
mode: 'auto',
}),
});
const tournee = await res.json();const stops: [number, number][] = [
[-114.07, 51.05],
[-113.98, 51.04],
[-114.12, 51.03],
[-114.04, 51.08],
];
const tournee = await unmap.router.optimize(stops)// 200, la forme que retournera ce point de terminaison une fois activé
{
"order": [0, 2, 3, 1],
"distanceMeters": 38200,
"durationSeconds": 3410,
"geometry": {
"type": "LineString",
"coordinates": []
}
}order: des indices dans lestopsque vous avez envoyé, dans l'ordre de visite. Le moteur déployé garde le premier et le dernier arrêt fixes; le corpus avec identifiants vérifie que l'ordre commence à 0, se termine à l'indice du dernier arrêt et contient chaque arrêt une fois.distanceMeters,durationSeconds,geometry: les mêmes champs que retourne/route, pour la tournée complète à travers chaque arrêt deorder. Un itinéraire optimisé est un itinéraire avec un champ de plus.
Limites. Au moins 3 et au plus 10 arrêts. Moins de 3 donne un 400 :
{ "error": "stops must be at least 3 lng,lat pairs", "code": "invalid_location" }, parce que
deux arrêts n'ont qu'un seul ordre possible et que la réponse cherchée est /route. Plus de 10
donne un 400 : { "error": "at most 10 stops", "code": "too_many_locations" }. Dix plutôt
qu'un nombre plus élevé que certaines API d'itinéraires annoncent, parce que l'optimiseur résout
une matrice N par N sur les arrêts en interne, et dix arrêts font 100 paires : exactement ce que
/matrix autorise déjà. Un plafond plus élevé serait une matrice plus grande contre une limite
que ce déploiement n'a pas mesurée. Un corps qui n'est pas
du JSON, qui n'est pas un objet JSON, ou qui dépasse 64 000 octets donne un 400 avec
code: "invalid_body", avant même que stops soit lu.
Erreurs observées. Le corpus avec identifiants exécute ces vérifications contre la passerelle
déployée à chaque poussée vers main :
- Quatre arrêts autour de Calgary donnent un
200avec un ordre complet et une géométrie. - Une tournée contenant Calgary et Lutselk'e échoue entièrement plutôt que de retourner une
tournée partielle, avec un
422et{ "error": "no optimized route: a stop is unreachable or beyond the engine's distance limit", "code": "no_route" }. - Le trajet Calgary-Lutselk'e mesure environ 1 400 km; le moteur atteint donc sa limite de distance avant que la connectivité routière puisse être isolée. Cela prouve que ce point de terminaison optimise une tournée de livraison, pas un territoire. Le plafond exact de l'optimiseur n'a pas été encadré séparément.
- Onze arrêts donnent un
400too_many_locationsà la passerelle, et deux arrêts donnent un400invalid_locationavec l'indication d'utiliser/route. - Toute autre défaillance en amont donne un
502aveccode: "routing_failed". Une réponse que la passerelle ne peut pas convertir en tournée valide donne aussi un502, jamais unorderinventé : un ordre d'apparence plausible mais faux serait pire qu'une erreur, puisqu'un répartiteur le ferait conduire.
Cas d'usage. Un répartiteur avec une camionnette et les livraisons d'une matinée autour d'une
même ville : huit adresses entrent comme stops, order revient comme la séquence pour les
livrer, et distanceMeters / durationSeconds donnent le total de la tournée, prêt à remettre à
un chauffeur avec la géométrie sur une carte.
Routage camion
Aperçu développeur. L'API et le corpus de validation sont en service, mais la couverture canadienne des restrictions demeure trop clairsemée pour une promesse de sécurité en disponibilité générale. La validation et la qualité des données continueront de progresser tout en gardant les formats de requête et de réponse stables.
mode=truck exécute le modèle de coût camion de Valhalla sur le même graphe : il hérite du
comportement automobile, puis exclut les routes dont la limite de hauteur, de largeur, de longueur,
de poids ou de charge par essieu est dépassée par votre véhicule, évite les routes interdites aux
poids lourds et peut privilégier les itinéraires désignés pour camions.
C'est inclus dans le routage normal, sur tous les forfaits, et l'a toujours été. Il en va de même pour tous les paramètres de gabarit ci-dessous. Un itinéraire camion compte pour un seul appel de votre forfait, sans multiplicateur ni compteur camion distinct.
Le module Truck Intelligence ajoute la partie coûteuse à bâtir : les
données gouvernementales canadiennes sur les restrictions, la validation selon le véhicule que vous
avez déclaré, et la déclaration explicite de couverture et de provenance. Sans lui,
/route?mode=truck renvoie toujours le même itinéraire, et le bloc truck déclare que rien n'a
été vérifié : chaque catégorie de coverage indique not_checked, accompagnée d'un avertissement
TRUCK_INTELLIGENCE_NOT_ENABLED. Ce point de terminaison ne refuse jamais à cause du module, et
les avertissements de fermeture n'en dépendent pas, sur aucun mode. Cette page indique exactement
quels territoires et quelles catégories de restrictions sont actifs aujourd'hui.
curl "https://api.unmap.dev/route?from=-114.0719,51.0447&to=-113.9871,51.0899&mode=truck&height=4.2&weight=36&axle_count=6" \
-H "Authorization: Bearer $UNMAP_API_KEY"const haul = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], {
mode: "truck",
truck: { height: 4.2, weight: 36, axleCount: 6 },
});| Paramètre | Unité | Par défaut | Accepté |
|---|---|---|---|
height | mètres | 4,11 | plus de 0, jusqu'à 10 |
width | mètres | 2,6 | plus de 0, jusqu'à 6 |
length | mètres | 21,64 | plus de 0, jusqu'à 50 |
weight | tonnes | 21,77 | plus de 0, jusqu'à 100 |
axle_load | tonnes | 9,07 | 0 à 40 |
axle_count | nombre | 5 | entier, 2 à 20 |
hazmat | booléen | false | true ou false |
use_truck_route | préférence | 0 | 0 (ignorer) à 1 (privilégier fortement) |
Chaque paramètre est facultatif et les valeurs par défaut sont celles de Valhalla, qui décrivent
un semi-remorque nord-américain typique. Une valeur hors plage donne un 400 qui nomme le
paramètre, de sorte qu'une confusion d'unités (pieds, livres) échoue bruyamment au lieu de produire
un itinéraire d'apparence plausible.
Ce que couvrent les données, honnêtement. Les restrictions proviennent d'OpenStreetMap, parce qu'aucun palier de gouvernement canadien ne publie un inventaire des dégagements ou des limites de charge par ouvrage, contrairement au National Bridge Inventory américain. Cela a des conséquences à prévoir dans votre conception, toutes mesurées sur l'extrait Canada du 2026-09-02 :
- Les limites de hauteur sont rares et inégales. Environ 20 000 tronçons portent un
maxheightsur quelque 100 000 tronçons de pont, la plupart en Nouvelle-Écosse. L'Ontario en compte environ 1 300. - Les limites de poids sont pratiquement absentes. Moins de 700 tronçons au pays portent un
maxweightet environ 500 une charge par essieu.weightetaxle_loadsont respectés partout où des données existent, c'est-à-dire presque nulle part pour l'instant, et aucun tronçon au Canada ne porte un nombre d'essieux, doncaxle_countn'exclut rien aujourd'hui. - Les restrictions conditionnelles ne sont pas appliquées. Les limites de charge au dégel, les interdictions horaires de camions et les affichages saisonniers ne sont pas modélisés.
- La préférence pour les routes de camionnage est une disposition, pas une garantie, et le haut
de la plage coûte cher.
use_truck_routesuit les tronçonshgv=designated, bien cartographiés en Colombie-Britannique et mal au Québec. Même à1, rien ne garantit que l'itinéraire empruntera une route désignée : le paramètre avantage les routes désignées et pénalise tout le reste, de sorte qu'il achète une préférence et se paie en distance. Mesuré sur un graphe d'essai au balisage désigné dense, un trajet est passé de 13,9 km àuse_truck_route=0à 33,2 km à1, et plusieurs autres ont à peu près doublé. Si vous utilisez1, attendez-vous à un itinéraire nettement plus long là où le réseau désigné est clairsemé ou indirect. Les valeurs intermédiaires sont le choix le plus prudent pour la plupart des appels, et0n'évite pas les routes de camionnage, il cesse simplement de les privilégier.
Une limite relève du moteur, pas des données. Les restrictions ne s'appliquent pas au premier
ni au dernier tronçon d'un itinéraire. Un véhicule doit pouvoir quitter l'endroit où il est
stationné, donc le tronçon de départ n'est jamais exclu, quelle que soit sa limite affichée. Un
véhicule de 4,2 m dont l'itinéraire part d'un point situé sur une rue limitée à 3,5 m sera envoyé
sur cette rue, sans erreur, et la même paire demandée en sens inverse ne donne aucun itinéraire.
Vérifiez vous-même le dégagement et la charge à l'origine et à la destination. Une réponse camion
porte ENDPOINT_RESTRICTIONS_UNCHECKED pour le signaler chaque fois que ces deux tronçons n'ont
pas pu être écartés, ce qui est le cas la plupart du temps mais pas toujours : lorsque la
vérification examine les deux et n'en trouve aucun restreint, l'avertissement est retenu plutôt que
répété par habitude.
C'est un routage adapté aux camions sur des données ouvertes, avec ses lacunes déclarées. Ce n'est pas un substitut à une vérification de permis ni un produit de prévention des collisions avec les ponts. Vérifiez les plans de charge hors normes auprès du système de permis provincial.
Itinéraires en transport en commun
mode=transit est en service pour le TTC sur un graphe Valhalla dédié. Une requête exige encore
depart_at. La réponse ajoute scheduleType: "scheduled" et des legs ordonnés pour la marche
d'accès, les trajets en transport et la marche de sortie. Chaque trajet porte l'agence, la ligne,
la destination, les heures prévues et les arrêts fournis par le flux. Les autres flux statiques
restent limités aux horaires. Le temps réel et arrive_by ne sont pas pris en charge. La liste
des flux et les lacunes restantes figurent sur la page
Couverture.
Ce que la réponse vous indique
Une réponse mode=truck porte un objet truck à côté des champs habituels. Il ne les encapsule
pas : le code qui lit distanceMeters aujourd'hui continue de fonctionner, et aucun autre mode
n'obtient ce champ.
{
"distanceMeters": 15627,
"durationSeconds": 1042,
"geometry": { "type": "LineString", "coordinates": [/* … */] },
"truck": {
"profile": { "height": 4.2, "weight": 36, "axle_count": 6 },
"profileComplete": false,
"coverage": {
"truck_network": "osm_only",
"physical_restrictions": "osm_only",
"bridge_weight": "osm_only",
"bridge_clearance": "osm_only",
"hazmat": "osm_only",
"conditional_restrictions": "not_modelled",
"temporary_restrictions": "not_checked"
},
"verification": "partial",
"warnings": [
{
"code": "INCOMPLETE_VEHICLE_PROFILE",
"message": "Vehicle dimensions were not fully specified (width, length, axle_load). …"
},
{
"code": "ENDPOINT_RESTRICTIONS_UNCHECKED",
"message": "Restrictions are not applied to the first and last road segment of a route. …"
},
{ "code": "WEIGHT_DATA_SPARSE", "message": "…" }
]
}
}profile: le véhicule que vous avez envoyé, dans les unités que vous avez employées. Il n'est jamais complété avec les valeurs par défaut : une dimension omise reste visiblement omise.profileComplete:trueseulement siheight,width,length,weightetaxle_loadont tous été fournis.axle_countn'est pas requis pour cela : le moteur respecte bien une limite du nombre d'essieux, mais aucun tronçon canadien n'en porte, donc l'omettre ne change rien.coverage: une valeur par classe de restriction, décrites ci-dessous.verification: le portrait global, dérivé decoverageet du profil plutôt que fixé à la main, afin que les deux ne puissent pas se contredire.warnings: à lire avant de vous fier à un itinéraire. Branchez surcode, qui est stable;messageest du texte et peut être reformulé.
Il n'y a délibérément aucun champ restrictions_applied. Annoncer une liste vide se lirait comme
« rien n'a restreint cet itinéraire », une affirmation que nous ne pouvons pas soutenir avant
d'avoir réexaminé les routes retenues tronçon par tronçon. Le silence est la réponse honnête, et
le champ apparaîtra lorsqu'il pourra être rempli fidèlement.
Valeurs de couverture. Si elles sont au nombre de cinq, c'est parce qu'« aucune restriction trouvée » et « aucune donnée à consulter » sont deux réponses différentes, et que les confondre en un seul mot est précisément ainsi qu'une API de routage finit par laisser croire qu'une route est dégagée.
| Valeur | Signification |
|---|---|
authoritative | Vérifié auprès d'une source gouvernementale pour cette classe |
osm_plus_authoritative | Une source gouvernementale couvre une partie du territoire, OpenStreetMap le reste |
osm_only | Données cartographiées par la communauté seulement. C'est une provenance, pas une note |
not_modelled | La règle existe dans le monde réel, mais le moteur ne peut pas l'exprimer |
not_checked | Rien n'a examiné cette classe |
Aujourd'hui, chaque classe est osm_only, sauf les restrictions conditionnelles, qui sont
not_modelled, et les restrictions temporaires, qui sont not_checked. Ces valeurs changeront
classe par classe à mesure que des sources officielles seront ajoutées, et le changement sera
visible ici plutôt que silencieux.
Valeurs de vérification. osm_only lorsque tout le portrait repose sur des données
communautaires et que votre profil était complet. partial lorsque votre profil était incomplet,
ou lorsqu'une source gouvernementale ne couvre que certaines classes. authoritative lorsque
chaque classe est vérifiée auprès du gouvernement, ce qu'aucun itinéraire n'atteint encore.
unknown et candidate_permit_route sont réservés aux itinéraires exigeant une vérification de
permis.
Codes d'avertissement.
| Code | Quand | Quoi faire |
|---|---|---|
INCOMPLETE_VEHICLE_PROFILE | Une dimension pouvant exclure une route n'a pas été fournie | Envoyez le profil complet. Les valeurs par défaut décrivent un autre camion que le vôtre |
ENDPOINT_RESTRICTIONS_UNCHECKED | Le premier ou le dernier tronçon n'a pas pu être écarté. Retenu lorsque les deux ont été examinés et qu'aucun n'est restreint | Vérifiez séparément le dégagement et la charge à votre origine et à votre destination |
WEIGHT_DATA_SPARSE | Vous avez envoyé weight ou axle_load | Ne lisez pas un itinéraire sans exclusion de poids comme un itinéraire homologué |
HAZMAT_CLASSES_NOT_MODELLED | Vous avez envoyé hazmat=true | Appliquez par-dessus vos propres règles par classe (TMD) et horaires |
Routage industriel
Le graphe est l'OpenStreetMap canadien plus les routes publiques de classe RRN déjà présentes
dans cet extrait. Les profils industriels (energy, agriculture, remote, mining)
changent les superpositions et la recherche, pas le moteur d'itinéraires. Il n'y a pas de graphe de routes d'hiver, pas de coût pour les routes de ressource, et pas de réseau d'accès
aux champs.
curl "https://api.unmap.dev/route?from=-114.0719,51.0447&to=-113.9871,51.0899&mode=auto&use_highways=0" \
-H "Authorization: Bearer $UNMAP_API_KEY"const rural = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], {
mode: "auto",
useHighways: 0,
});use_highways est la seule préférence que le graphe peut honorer aujourd'hui : OSM classe déjà
les autoroutes et les routes nationales. La mettre à 0 éloigne le même moteur de routes
publiques de ces classes. Elle ne privilégie pas une route de bail, n'ouvre pas une route
d'hiver et ne trouve pas une entrée de champ.
Ce qu'un routage industriel ultérieur exigerait, et pourquoi ces paramètres ne sont pas exposés :
| Demande | Pourquoi ce n'est pas un paramètre |
|---|---|
| Routes de ressource / industrielles / de bail | L'accès est autorisé et souvent saisonnier. Un track / service OSM n'est pas un graphe de baux. |
| Routes d'hiver, de glace, saisonnières | Ouvert/fermé est une date, pas une géométrie. Aucun extrait national sous licence n'est dans le graphe. |
| Accès aux champs / entrées de ferme | Les entrées ne sont pas des tronçons highway OSM fiables. |
avoid: ["restricted"] ou avoid: ["winter"] | Le graphe ne modèle pas la restriction. Un drapeau inventerait un itinéraire. |
Ne traitez pas un préréglage profile comme entrée de
routage. Voir Couverture et Profil éloigné.
Évitement
avoid tient un trajet à l'écart des péages, des autoroutes ou des traversiers :
curl "https://api.unmap.dev/route?from=-79.3832,43.6532&to=-79.8711,43.2557&avoid=tolls" \
-H "Authorization: Bearer $UNMAP_API_KEY"const sansPeage = await router.route([-79.3832, 43.6532], [-79.8711, 43.2557], {
avoid: ["tolls"],
});C'est une préférence, pas une garantie, et la nuance compte. En dessous, ce sont des poids de coût et non des exclusions : demander d'éviter les péages rend chaque route à péage extrêmement coûteuse pour le moteur, mais si le seul passage est un pont à péage, le trajet emprunte le pont à péage. La réponse ne dit pas qu'il l'a fait.
avoid est donc le bon outil pour « prends le trajet gratuit s'il en existe un » et le mauvais
outil pour « ce véhicule n'a pas le droit d'emprunter les routes à péage ». Pour une règle stricte,
comparez la géométrie renvoyée aux routes qui vous intéressent, ou utilisez « Zones à éviter »
plus bas, qui en est une.
| valeur | ce qu'elle pénalise | modes |
|---|---|---|
tolls | routes et ponts à péage | auto, car, truck |
highways | autoroutes et routes nationales | auto, car, truck |
ferries | traversées en traversier | les cinq |
Combinez-les avec des virgules : avoid=tolls,ferries. Demander deux fois la même valeur revient
à la demander une fois. Une valeur hors du tableau donne un 400 plutôt qu'un mot ignoré en
silence, et il en va de même si vous demandez à un bicycle d'éviter les péages, car un vélo n'a
aucun réglage de péage à baisser.
avoid=highways et use_highways sont le même réglage : l'un est le bout de la molette, l'autre
est la molette. Envoyer les deux donne un 400 même s'ils concordent, pour qu'il y ait une seule
réponse à « qu'est-ce que j'ai demandé » plutôt qu'une règle de priorité à retenir.
Il n'y a pas de unpaved. C'est la quatrième entrée évidente et elle manque volontairement.
Le moteur de conduite n'a aucune notion de revêtement; ce qui s'en rapproche le plus couvre les
chemins agricoles et forestiers, ce qui ne correspond pas à la façon dont le gravier rural
canadien est étiqueté. Le vélo, lui, a un vrai réglage de revêtement : un seul avoid=unpaved
aurait donc voulu dire deux choses différentes selon le mode, et la version conduite aurait été
proche d'un réglage sans effet qui ressemble à une fonctionnalité. Elle reviendra quand nous
pourrons dire ce qu'elle couvre.
Zones à éviter
Pas encore activé. POST /route et POST /isochrone renvoient 404 tant que
cette fonctionnalité est désactivée. Sur /matrix, /match et /optimized-route,
envoyer avoid_areas renvoie 400 avec le code invalid_route_option. Les exemples
ci-dessous seront disponibles après validation du moteur et activation.
Tenez un trajet à l'écart d'une fermeture, d'un chantier ou d'une zone d'évacuation en envoyant la géométrie :
curl -X POST "https://api.unmap.dev/route" \
-H "Authorization: Bearer $UNMAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": [-114.0719, 51.0447],
"to": [-113.9871, 51.0899],
"avoid_areas": {
"type": "Polygon",
"coordinates": [[[-114.05,51.055],[-114.03,51.055],[-114.03,51.065],[-114.05,51.065],[-114.05,51.055]]]
}
}'// Tout Polygon, MultiPolygon ou FeatureCollection GeoJSON.
import type { AvoidAreas } from "@unmap/routing";
const fermeture: AvoidAreas = {
type: "Polygon",
coordinates: [[[-114.05, 51.055], [-114.03, 51.055], [-114.03, 51.065], [-114.05, 51.065], [-114.05, 51.055]]],
};
const contournement = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], {
avoidAreas: fermeture,
});Aperçu privé, actuellement indisponible. Valhalla définit les polygones exclus comme des
contraintes, mais la version 3.8.3 peut placer un point de départ ou d'arrivée de l'autre côté
d'un polygone. AVOID_AREAS_ENABLED reste donc désactivé et ces requêtes donnent un 404. Nous
n'activerons la route qu'après le déploiement d'un correctif du moteur et la réussite des essais
réels de contournement, de destination encerclée et de limite publique.
Cela voyage dans un corps de requête, car un polygone n'entre pas dans une chaîne de requête.
POST /route et POST /isochrone prennent les mêmes paramètres que leurs formes GET et
renvoient les mêmes réponses; la bibliothèque cliente bascule d'elle-même quand vous passez
avoidAreas, et reste en GET sinon. Envoyer avoid_areas en paramètre d'URL donne un 400 qui
nomme POST plutôt que de l'ignorer. /matrix, /match et /optimized-route avaient déjà un
corps et le prennent directement.
Les anneaux intérieurs sont refusés. Le moteur ne peut pas préserver un trou dans un polygone
exclu. L'API renvoie donc 400 invalid_location au lieu d'élargir ou d'inverser silencieusement
votre demande.
Une fois la fonction activée, les limites seront de 8 zones, 100 sommets et 10 km de périmètre au
total. Les anneaux doivent être fermés, respecter les plages valides de longitude et de latitude
et délimiter une surface. Une limite de taille dépassée donne un 400 de code
too_many_locations; une géométrie mal formée donne un 400 de code invalid_location.
Dénivelé
Les itinéraires et les isochrones en bicycle et pedestrian tiennent compte du relief. Chaque
arête du graphe porte une pente, mesurée tous les 60 m à partir du même modèle d'élévation
national à 30 m qui ombre les styles outdoor, et les temps de vélo et de marche en tiennent
compte.
La conduite, non. Les modèles de coût auto et truck de Valhalla n'ont aucun terme de
pente : franchir le col Rogers coûte exactement ce que coûte la même distance en Saskatchewan.
C'est le modèle du moteur, pas une lacune de nos données d'élévation, et c'est pourquoi
use_hills renvoie un 400 sur les modes de conduite au lieu d'être ignoré en silence.
use_hills oriente le moteur vélo et marche :
curl "https://api.unmap.dev/route?from=-123.1207,49.2827&to=-123.0700,49.3400&mode=bicycle&use_hills=0" \
-H "Authorization: Bearer $UNMAP_API_KEY"const doux = await router.route([-123.1207, 49.2827], [-123.0700, 49.3400], {
mode: "bicycle",
useHills: 0,
});0 contourne une montée par un chemin plus long mais plus plat ; 1 prend la ligne directe. L'effet
est le plus marqué là où le relief se trouve : attendez-vous à une différence visible à Vancouver,
sur l'escarpement de Calgary et dans les collines de la Gatineau, et à très peu dans les Prairies.
Le modèle d'élévation est le MRDEM-30 de RNCan, à 30 m jusqu'à 85 degrés nord. La plupart des moteurs de routage utilisent le SRTM, qui s'arrête à 60 degrés nord ; le nôtre a du vrai relief à Whitehorse, Yellowknife et Iqaluit.
Il n'y a pas de profil d'élévation dans la réponse ni de point de terminaison d'altitude. Pour le relief lui-même, les tuiles de terrain servent directement le même modèle.
Fraîcheur du graphe
curl "https://api.unmap.dev/routing/status" \
-H "Authorization: Bearer $UNMAP_API_KEY"{
"region": "canada",
"status": "ready",
"engine": { "name": "valhalla", "version": "3.8.3" },
"graph": {
"builtAt": "2026-09-10T04:12:00.000Z",
"ageHours": 176,
"osmTimestamp": "2026-09-09T20:21:13.000Z"
}
}statusvautreadyouunavailable. L'appel renvoie200dans les deux cas : un point de terminaison d'état qui échoue quand le routage est en panne n'apprend rien qu'un/routeen échec n'ait déjà dit.graphest absent quand le moteur n'a pas indiqué de date de construction. Absent veut dire inconnu. Cela ne veut pas dire récent, et aucune date de remplacement ne prend sa place.ageHoursest le nombre d'heures entières depuisbuiltAt, arrondi vers le bas.osmTimestampest la date à laquelle l'extrait OSM source était à jour, lue dans l'en-tête du PBF au moment de la construction. Il est absent tant qu'un graphe construit par le pipeline actuel n'est pas déployé. Absent veut dire inconnu. Il n'est pas inventé à partir de la date de construction du jeu de tuiles.
Mis en cache cinq minutes, donc l'interroger depuis un tableau de bord coûte peu.
Il n'y a pas de mise à jour quotidienne, et cette page ne le dira pas tant qu'il n'y en aura
pas. Le graphe Canada est reconstruit à la main, car une construction complète avec dénivelé ne
tient pas dans le plafond de six heures d'un exécuteur d'intégration continue hébergé; le
raisonnement et les mesures sont dans
docs/decisions/2026-09-17-routing-freshness-is-not-nightly.md. Ce point de terminaison existe
pour que vous voyiez le vrai chiffre plutôt que de croire une pastille sur parole. Un graphe
annoncé vieux de 40 jours aide davantage à décider si un trajet est fiable qu'une promesse de
rafraîchissement nocturne.
La date de construction du jeu de tuiles borne toujours l'ancienneté possible d'un trajet
construit. osmTimestamp répond à l'autre moitié : l'âge des routes elles-mêmes au moment
où ce graphe a été construit.
Notes sur route et isochrone
Couverture et cas d'échec
Le graphe est construit à partir des données OpenStreetMap canadiennes, et de rien d'autre. Un graphe limité au Canada est une contrainte assumée, pas un oubli : tout le conteneur d'itinéraires doit tenir sous le plafond de mémoire des conteneurs Cloudflare, et un graphe nord-américain n'y entre pas.
Concrètement, tout ceci a été vérifié contre l'API en production le 2026-09-02 :
- Une extrémité située hors du Canada échoue. Calgary vers Seattle donne un
422, et non un itinéraire tronqué à la frontière. Le calcul transfrontalier n'est pas pris en charge du tout. - Un point en pleine eau ou hors du réseau routier échoue de la même façon, avec un
422. - Être au Canada n'est pas la même chose qu'être relié par la route. Calgary vers Iqaluit donne un
422, puisqu'Iqaluit n'a aucun lien routier avec le continent. Un itinéraire dans Iqaluit fonctionne très bien. - Valhalla applique sa propre distance maximale par mode, et celle de la marche est bien plus
courte que celle de la voiture. Un trajet transcanadien en
mode=autoréussit là où la même paire enmode=pedestrianretourne un422. - Sur
/matrix, la même lacune de couverture apparaît comme un422pour toute la requête, exactement comme sur/route, plutôt que comme une cellulenullpour la seule paire touchée. Voir « Matrix » plus haut./optimized-routedevrait échouer de la même façon pour la même raison (il résout une matrice en dessous), mais ce n'est pas encore confirmé contre la passerelle déployée; voir « Optimize » plus haut.
Le reste
modeatteint réellement le moteur de coût. Marcher une paire de points donnée prend mesurablement plus de temps que la conduire : ce n'est pas une simple étiquette. Sur l'exemple calgarien plus haut,autoretourne 991 secondes etpedestrianen retourne 8 198, sur un trajet pourtant plus court.- Il existe deux
503, et ce ne sont pas les mêmes. Si le service d'itinéraires n'est pas branché du tout, vous en obtenez un décrivant la forme de requête attendue, plutôt qu'un400portant sur des paramètres qui n'ont jamais été le vrai problème : la vérification du service passe avant la validation des paramètres précisément pour cette raison, et en production vous ne devriez jamais le voir (c'est ce que vous voyez en exécutant la passerelle en local sans le conteneur). Si le conteneur démarre ou est bloqué, vous obtenez plutôt{ "error": "routing temporarily unavailable", "code": "service_unavailable", "retryable": true }avecRetry-After: 5. Celui-là vaut une nouvelle tentative; l'autre non. - Les isochrones coûtent plus cher que tout le reste de la plateforme. Une requête à trois contours
à chaud a mesuré une médiane de 1490 ms le 2026-08-03 et environ 700 ms le 2026-09-02, depuis un
seul client sur l'internet public, contre environ 130 à 220 ms pour les tuiles, le géocodage et
/route. Prévoyez-le si vous appelez ce point de terminaison de façon interactive. - Les cinq points de terminaison envoient
Cache-Control: private, max-age=3600, mais l'en-tête ne prend effet que sur/routeet/isochrone. Sur ces deux-là,privatesignifie que votre propre client peut réutiliser une réponseGETpendant une heure, mais que les caches partagés et les CDN ne le doivent pas, car les coordonnées d'une requête d'itinéraire se répètent bien moins entre appelants qu'une requête de géocodage ou une tuile./matrix,/matchet/optimized-routesont desPOST, et selon la RFC 9111 une réponse mise en cache pour unPOSTne peut satisfaire qu'unGETultérieur sur la même URI, jamais un autrePOST: l'en-tête ne fait donc rien sur ces trois réponses, et chaque appel à matrix, match ou optimize s'exécute en entier, peu importe la fraîcheur du même corps déjà envoyé. Contrairement au géocodage, il n'y a ici non plus aucun cache à notre périphérie : une réutilisation se produit entièrement dans votre client, n'atteint donc jamais la passerelle et n'est jamais facturée. Les réponses d'erreur ne portent aucune directive de cache. - Les cinq points de terminaison sont comptabilisés sous
routingdans votre tableau de bord d'utilisation. Un itinéraire, une isochrone, une matrice, un appariement et une optimisation comptent chacun pour un appel, quel que soit le nombre de contours, de coordonnées ou d'arrêts portés par la requête.
Depuis la bibliothèque cliente
@unmap/routing enveloppe les cinq points de terminaison, et @unmap/sdk expose le même objet
sous unmap.router :
import { Router, RouterError } from '@unmap/routing'
const router = new Router({ key: 'um_live_...' })
const route = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: 'auto' })
route.distanceMeters // 15627
const haul = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], {
mode: 'truck',
truck: { height: 4.2, weight: 36, axleCount: 6 },
})
const bands = await router.isochrone([-114.0719, 51.0447], { minutes: [10, 20], mode: 'pedestrian' })
bands.features // entités GeoJSON, typées unknown[]
const times = await router.matrix([[-114.0719, 51.0447]], [[-113.9871, 51.0899], [-114.1, 51.05]])
times.durations[0]![1] // secondes; 0 est une vraie réponse sur la diagonale. Si UNE SEULE
// paire est inaccessible ou dépasse la limite de distance propre à Valhalla, tout l'appel
// échoue avec une RouterError (statut 422, code 'no_route') plutôt que de retourner une
// cellule null.
const cleaned = await router.match([[-114.0719, 51.0447], [-114.0705, 51.0451], [-114.069, 51.0458]])
cleaned.distanceMeters // la distance routière réelle, après accrochage
const tournee = await router.optimize([
[-114.07, 51.05], [-113.98, 51.04], [-114.12, 51.03], [-114.04, 51.08],
])
tournee.order // ex. [0, 2, 3, 1] : des indices dans les arrêts passés, dans l'ordre de visite.
// Le premier et le dernier arrêt sont le départ et l'arrivée fixes. Si un arrêt est
// inaccessible, ou si l'ensemble est réparti au-delà de la limite de distance propre à
// Valhalla, tout l'appel échoue avec une RouterError (statut 422, code 'no_route') plutôt
// que de retourner une tournée partielle.route()etmatch()prennent des tuples[lng, lat];matrix()prend deux tableaux de tuples, origines puis destinations;optimize()prend un seul tableau de tuples, les arrêts. Le client masque le fait que les cinq points de terminaison écrivent leurs coordonnées différemment sur le fil.modeest typé'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck' | 'transit'dans le@unmap/routingpublié, si bien qu'une faute de frappe est attrapée à la compilation autant que par le400de la passerelle.useHillsdevientuse_hillssur le fil, commeuseHighwaysdevientuse_highways.avoidAreasdevientavoid_areassur le fil, et le passer fait basculerroute()etisochrone()deGETàPOST. Mêmes paramètres, même réponse.avoidest un tableau dans la bibliothèque sur tous les points de terminaison (avoid: ["tolls"]). Sur les deux points GET il part sous forme de liste séparée par des virgules; sur les trois points POST il reste un tableau. Les deux atteignent le même validateur, donc ils veulent dire la même chose.router.status()correspond àGET /routing/status, et se résout avecstatus: "unavailable"au lieu d'échouer quand le routage est en panne.- Le profil de camion est un objet
truckaux champs en camelCase (axleLoad,axleCount,useTruckRoute, plusheight,width,length,weight,hazmat); le client les convertit en champs snake_case attendus par la passerelle, dans la chaîne de requête pourroute/isochroneet dans le corps JSON pourmatrix/match/optimize. useHighwaysest le même drapeau de 0 à 1 queuse_highwayssur le fil. Omis, le client n'envoie rien, et Valhalla garde sa valeur par défaut.minutesest obligatoire surisochrone, sans valeur par défaut. L'objet d'options des quatre autres méthodes est entièrement facultatif.- Les cinq méthodes acceptent un
AbortSignalsoussignal. - Une réponse non 2xx lève une
RouterErrorqui porte le statut HTTP dans.status, et lecodelisible par machine de la passerelle dans.codelorsque le corps en portait un, pour n'importe laquelle des cinq méthodes. route()etmatch()retournent unRouteResultentièrement typé.matrix()retourne unMatrixResult(durationsetdistances, chacun(number | null)[][]).optimize()retourne unOptimizedRouteResult, unRouteResultplusorder: number[].isochrone()retourne uneFeatureCollectionvolontairement minimale, dont lesfeaturessont desunknown[]: convertissez-la vers vos propres types GeoJSON (ou ceux de votre bibliothèque cartographique) si vous voulez que les propriétés ci-dessus soient vérifiées à la compilation.
Prochaines étapes
- L'étape 6 du Guide de démarrage trace un de ces itinéraires sur une carte en quelques lignes.
- Erreurs pour chaque statut que ces points de terminaison peuvent retourner, et les corps qu'ils envoient.
- Composants pour un panneau d'itinéraire prêt à copier, bâti sur cette API.