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", "code": "invalid_mode" }.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", "code": "invalid_route_option" }. 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", "code": "invalid_route_option" }. Voir « Dénivelé » plus bas pour savoir pourquoi la conduite est exclue.steps(facultatif,falsepar défaut) :trueajoutesteps, les indications écrites virage par virage. Voir Indications virage par virage.lang(facultatif, seulement avecsteps=true) :enoufr, la langue des indications. Sans ce paramètre, la passerelle litAccept-Language, puis se rabat suren.
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. Les indications virage par virage sont sur demande : voir Indications virage par virage.
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", "code": "invalid_location" }. Une moitié vide, comme dans from=,, donne aussi
un 400. Une longitude ou une latitude hors du WGS84 donne un 400 de code invalid_location.
Une paire au Canada que Valhalla ne peut pas relier donne un 422 avec
{ "error": "no route", "code": "no_route" }. Une extrémité hors du Canada est un autre 422,
{ "error": "outside Canadian coverage", "code": "outside_coverage" }, et le graphe n'est pas
appelé. Toute autre défaillance en amont donne un 502 avec
{ "error": "route failed", "code": "upstream_error" }; branchez donc sur code et le statut.
Indications virage par virage
Ajoutez steps=true et l'itinéraire porte steps : une entrée par manœuvre, dans l'ordre du
trajet, rédigée pour être affichée en liste à côté de la carte. Il n'y a pas de narration vocale.
/match et /optimized-route acceptent la même option, sous la forme "steps": true dans le
corps.
curl "https://api.unmap.dev/route?from=-114.0719,51.0447&to=-113.9871,51.0899&steps=true&lang=fr" \
-H "Authorization: Bearer $UNMAP_API_KEY"const route = await router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { steps: true, lang: "fr" });
for (const step of route.steps ?? []) console.log(step.instruction, step.distanceMeters);Chaque indication a cette forme. Les valeurs ci-dessous sont illustratives, pas une réponse enregistrée :
{
"type": "turn", // l'action; choisissez l'icône d'après ce champ
"modifier": "left", // la direction; absent quand il n'y en a pas
"instruction": "Tournez à gauche sur 4 Street Southwest.",
"streetNames": ["4 Street Southwest"],
"distanceMeters": 800, // le tronçon couvert par l'indication
"durationSeconds": 74,
"location": [-114.0717, 51.0381], // où l'indication commence
"geometryRange": [41, 63] // premier et dernier indice dans geometry.coordinates
}typeest un nom propre à unmap, pour que votre code ne dépende pas de la numérotation du moteur :depart,arrive,turn,uturn,continue,keep(rester d'un côté à une bifurcation),merge,name_change(la route change de nom),ramp,exit,roundabout,roundabout_exit,ferry,ferry_exit, les étapes de transport en communtransit(monter),transit_transfer,transit_stay_on,station_enter,station_transfer,station_exit, et les étapes de marcheelevator,stairs,escalator,building_enter,building_exit.otherest une étape que le moteur a produite et qu'unmap ne nomme pas encore; affichez son instruction.modifiervautsharp_left,left,slight_left,straight,slight_right,rightousharp_right. Surdepartetarrive, c'est le côté de la route; suruturn, le sens du demi-tour.geometryRangeest inclusif.geometry.coordinates.slice(début, fin + 1)est le tracé à mettre en évidence quand on survole l'indication, etlocationen est le premier point.roundaboutExit(surroundabout) indique quelle sortie prendre, en comptant depuis l'entrée.signreprend un panneau d'autoroute tel qu'affiché :exitNumbers,branches(la route sur laquelle mène la sortie),towardetnames. Chaque liste est absente quand le panneau n'en porte pas.
Langue. lang=en ou lang=fr. Une étiquette régionale se résout par sa langue, donc
fr-CA donne le français, et toute autre langue donne l'anglais plutôt qu'un 400. Sans
lang, la passerelle lit l'en-tête Accept-Language de la requête, que le navigateur envoie
pour vous, et la réponse indique la langue utilisée dans Content-Language. Le texte français
vient de la locale française de France du moteur d'itinéraires (il n'a pas de locale
canadienne-française) : quelques tournures sont donc européennes.
Transport en commun. Avec mode=transit, les indications couvrent la marche vers chaque
arrêt et depuis celui-ci, ainsi que les trajets, en plus des legs existants.
Coût. Les indications sont désactivées par défaut parce qu'elles alourdissent la réponse. Elles restent légères à côté du tracé lui-même, qui n'est pas simplifié.
Erreurs. Une valeur de steps autre que true ou false donne un 400. De même pour
lang sans steps=true : { "error": "lang applies only with steps=true", "code": "invalid_route_option" }.
/isochrone et /matrix n'ont pas de trajet à décrire : l'un ou l'autre paramètre y donne un
400 avec le même code.
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
near(obligatoire) : le point central sous la formelng,lat, l'écriture qu'utilisent aussi/places/nearby,/transit/stopset/incidents.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,pedestrian,truckoutransit, validé exactement comme sur/route, et acceptant les mêmes paramètres de profil de camion.depart_at(mode=transitseulement) : le départ à partir duquel les bandes sont calculées, la même valeur ISO 8601 que prend/route. Obligatoire en transport en commun et refusé pour tout autre mode.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. Le transport en commun les refuse, comme sur/route.
curl "https://api.unmap.dev/isochrone?near=-114.0719,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.
Bandes en transport en commun. mode=transit marche et prend les services prévus à partir de
depart_at, sur le même graphe et avec le même calcul de coût que /route?mode=transit : un
endroit compris dans une bande est un endroit que /route atteint dans ce temps. Attendez-vous à
un MultiPolygon : un trajet vous dépose à un arrêt, et la zone accessible à pied à partir de là
forme souvent son propre îlot, de l'autre côté d'une rivière ou quelques kilomètres plus loin que
le corridor. Tous les îlots sont conservés.
curl "https://api.unmap.dev/isochrone?near=-79.3806,43.6452&minutes=30,60&mode=transit&depart_at=2026-10-15T08:30" \
-H "Authorization: Bearer $UNMAP_API_KEY"Une requête en transport en commun coûte plus cher qu'une requête routière : environ 1,8, 2,6 et 3,4 secondes pour une seule bande de 30, 60 et 120 minutes à partir du centre-ville de Toronto (médiane de cinq requêtes en production le 2026-10-08, contre environ 0,5 seconde pour une bande de marche de 15 minutes). Prévoyez un délai d'attente en conséquence.
// 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", "code": "invalid_route_option" }. - Toute valeur au-dessus de 120 :
{ "error": "minutes must be 120 or less", "code": "invalid_route_option" }.
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 near absent ou non numérique, donne un
400 :
{ "error": "near=lng,lat and minutes are required", "code": "invalid_location" }.
Un near= vide donne aussi un 400. Une longitude ou une latitude hors du WGS84
donne un 400 de code invalid_location. Un centre hors du Canada donne un 422 :
{ "error": "outside Canadian coverage", "code": "outside_coverage" }, et le moteur n'est pas
appelé. Un centre mode=transit sans service prévu à proximité reçoit le même code avec son propre
message, { "error": "no scheduled transit serves this point; use a road mode such as mode=pedestrian", "code": "outside_coverage" },
plutôt qu'un polygone de marche présenté comme une bande en transport en commun. « À proximité »
reste grossier : le moteur cherche du transport en commun dans la tuile de routage environnante,
d'environ 25 km de côté, de sorte qu'un centre à plusieurs kilomètres de l'arrêt le plus proche
dans une zone desservie obtient encore des bandes surtout pédestres. Toute défaillance en amont, y
compris un centre canadien que le moteur refuse, donne un 502 :
{ "error": "isochrone failed", "code": "upstream_error" }.
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 : Authorization: Bearer,
X-API-Key, X-Goog-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. - Une cellule peut valoir
null, et le reste de la grille répond quand même. Une paire que le moteur n'a pas pu relier est mise ànullsur place ; la ligne garde sa longueur et chaque autre cellule garde son sens, doncdurations[i][j]correspond toujours à l'origineivers la destinationj. Géreznullsur chaque cellule. Une matrice réelle sur Moosonee, Cochrane et Timmins retourne[[0, null, null], [null, 0, 4735], [null, 5415, 0]]: six cellules nulles, deux qui répondent, un vrai0sur la diagonale. Ceci corrige une version antérieure de cette page, qui affirmait qu'une cellulenulln'apparaissait jamais. - N'affichez pas un
nullcomme « inaccessible » à un utilisateur. Cela signifie que le moteur n'a pas retourné de réponse pour cette paire, et aujourd'hui c'est généralement parce qu'il a abandonné, pas parce qu'aucune route n'existe. Sur 40 paires sondées à travers le pays, chaquenullcorrespondait à une paire que/routecalcule avec une vraie distance routière, y compris les cellules Moosonee ci-dessus. Traiteznullcomme « pas de réponse pour cette paire », et repliez-vous sur/routesi vous devez savoir si une route existe. - Une requête réellement incalculable échoue avec un
422, et non par une cellule nulle, pour l'une de trois raisons : deux points séparés à vol d'oiseau de plus de 400 km en voiture ou en camion (200 km à pied ou à vélo) ; un point sans aucune route à proximité, par exemple en pleine eau ; ou un ensemble de points qui ne sont pas tous dans une même région reliée par le réseau routier. Cette dernière cause concerne l'ensemble, pas une coordonnée en particulier : chaque point d'une requête rejetée peut être valide isolément, donc il n'y a pas de point fautif unique à chercher. Retirez ou séparez plutôt le point isolé. À vol d'oiseau, pas en distance routière : Calgary vers Edmonton fait 300 km de route et répond sans problème.
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 : le plafond par défaut du moteur est
de 2 500 paires et une requête 10 par 10 en fait 100, donc le 400 de la passerelle survient
toujours en premier. C'est un plancher, 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 coordonnée hors du WGS84 donne un 400 de code
invalid_location. Un point hors du
Canada donne un 422 { "error": "outside Canadian coverage", "code": "outside_coverage" }, et
le moteur n'est pas appelé. Un point canadien sans route à proximité, un ensemble de points que
le réseau routier ne relie pas, ou deux points au-delà de la limite à vol d'oiseau, donne un
422 avec
{ "error": "no matrix: a location has no road near it, the locations are not all in one connected region, or two are further apart than the engine's straight-line matrix limit", "code": "no_route" }.
Une paire que le moteur ne fait que rater ne donne pas cette erreur : elle revient sous forme de
cellule null dans un 200. Toute autre défaillance en amont donne un 502 avec
code: "upstream_error".
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.stepsetlang(facultatifs) : les mêmes indications virage par virage que/route, avecstepsen booléen JSON.
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". Une coordonnée hors du WGS84 donne un 400
de code invalid_location. Une coordonnée hors du Canada donne un 422
{ "error": "outside Canadian coverage", "code": "outside_coverage" }, et la trace n'est pas
accrochée. 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, qui est au
Canada, renvoie ce 422). Toute autre défaillance en amont donne toujours un 502 avec
code: "upstream_error".
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.
En service sur api.unmap.dev. Comme /matrix et /match, 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 /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.stepsetlang(facultatifs) : les mêmes indications virage par virage que/route, avecstepsen booléen JSON.
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: "upstream_error". 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.
Plus d'un véhicule
/optimized-route ordonne les arrêts d'un seul véhicule, rien de plus. Quand plusieurs véhicules se
partagent le travail, ou que les fenêtres horaires, les capacités de charge ou les pauses des
chauffeurs décident qui va où, utilisez plutôt Unmap Optimize :
POST /optimize prend des envois et un parc de véhicules dans le format de requête de l'API Route
Optimization de Google Maps Platform, et répond avec une tournée par véhicule. Dans la bibliothèque cliente, c'est optimizeTours() (plus bas), une
méthode distincte d'optimize(). Ses champs pris en charge
sont sur la même page, ses erreurs dans la référence des erreurs,
et ses exemples dans la galerie d'exemples.
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 sur un graphe Valhalla dédié, sur /route et /isochrone. Une
requête exige encore depart_at. La réponse d'itinéraire 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 : distanceMeters reste au premier niveau, 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 (la couche roads du fond de carte est aussi de la visualisation). 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 échantillonnée lors de sa construction, et les temps de vélo et
de marche en tiennent compte. Le graphe conserve sa source d'altitude enregistrée lorsque le MNE
actif de la carte change.
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 du graphe conservé est le MNEMR-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.
La réponse d'itinéraire n'a pas de profil d'altitude. L'API d'altitude fournit des échantillons du MNE actif, et les tuiles de terrain son relief. Avec la publication mondiale 6 active, ces services utilisent une source différente de celle d'un graphe routier construit auparavant.
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. La limite de distance et la matrice ont été vérifiées contre l'API en production le
2026-09-02, de même que les statuts 422 pour la pleine eau et pour Calgary vers Iqaluit.
outside_coverage est le corps que cette version retourne pour un ancrage hors du Canada; cette
vérification ne faisait pas partie de la lecture du 2026-09-02, et la production n'a pas été
relue pour ce code.
- Une extrémité hors du Canada donne un
422{ "error": "outside Canadian coverage", "code": "outside_coverage" }. Calgary vers Seattle retourne ce corps. Le graphe n'est pas interrogé. Le calcul transfrontalier n'est pas pris en charge. Une longitude ou une latitude hors du WGS84 est un400invalid_location, et non ce code. - Un point en pleine eau ou hors du réseau routier, à l'intérieur du Canada, donne un
422{ "error": "no route", "code": "no_route" }. La baie d'Hudson est dans le masque de couverture, donc une trace là estno_route. La lecture du 2026-09-02 a consigné ce cas comme un422. - Être au Canada n'est pas la même chose qu'être relié par la route. Calgary vers Iqaluit donne
un
422no_route, puisqu'Iqaluit n'a aucun lien routier avec le continent. Un itinéraire dans Iqaluit fonctionne. Le422de cette paire a été confirmé le 2026-09-02. - 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, une paire que le moteur ne parvient pas à relier apparaît comme une cellulenull, le reste de la grille répondant normalement. Un ensemble de points que le réseau routier ne relie réellement pas fait plutôt échouer toute la requête avec un422no_route, car cette vérification a lieu avant qu'une seule cellule existe. Un point hors du Canada estoutside_coverageavant l'une ou l'autre de ces réponses. Voir « Matrix » plus haut./optimized-routen'a droit au traitement par cellule dans aucun des deux cas : une tournée a besoin de chaque segment, donc un arrêt que les autres ne peuvent pas atteindre fait échouer toute la requête, même si unnulldans la matrice sous-jacente ne suffit pas à la faire échouer.
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 durée valide, et null signifie que cette paire n'a pas pu être reliée.
// Tout l'appel échoue avec RouterError (422, 'no_route') pour des points hors du réseau routier,
// des régions non reliées ou un ensemble dépassant la limite de distance des matrices du moteur.
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 les arrêts sont dans
// des régions non reliées, hors du réseau routier ou au-delà de la limite de distance des
// matrices du moteur, 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. router.optimizeTours(request)est Unmap Optimize, pas l'une des cinq : elle prend unOptimizeToursRequestet renvoie{ response, warnings, billableShipments, timeoutClamped, clampedTimeoutSeconds }, et un échec lèveUnmapOptimizeError. Elle est offerte depuis la 0.21.0. Voir API Optimize.- 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.