Aller au contenu

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é :

résultat en direct
le code qui l’a produit
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 },
  });
});
npm i @unmap/routing

@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 un 400, pas une étape.
  • mode (facultatif, auto par défaut) : auto, car, bicycle, pedestrian, truck ou transit. car et auto désignent le même profil automobile; car existe parce que auto se lit comme « automatique » pour qui ne connaît pas Valhalla. Toute autre valeur donne un 400 : { "error": "mode must be one of auto, car, bicycle, pedestrian, truck, transit" }. Jusqu'au 2026-09-03, une valeur non reconnue retombait silencieusement sur auto; ce n'est plus le cas.
  • depart_at (mode=transit seulement) : une date et heure ISO 8601. Une valeur avec un décalage ou Z dé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 option arrive_by.
  • Profil de camion (facultatif, mode=truck seulement) : height, width, length en mètres, weight et axle_load en tonnes, axle_count en nombre entier, hazmat à true ou false, et use_truck_route de 0 à 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 que truck donne un 400.
  • avoid (facultatif) : une liste séparée par des virgules parmi tolls, highways, ferries. Une préférence, pas une exclusion : lisez « Évitement » plus bas avant de vous y fier. tolls et highways ne valent que pour les modes de conduite; ferries vaut 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 un 400. avoid=highways et use_highways sont le même réglage et ne peuvent pas être envoyés ensemble.
  • use_highways (facultatif, modes de conduite seulement) : de 0 à 1. Disposition à emprunter les autoroutes et les routes nationales. 0 les évite, 1 les privilégie. Omise, Valhalla utilise sa propre valeur par défaut (0,5). Accepté sur auto, car et truck. Sur bicycle ou pedestrian, c'est un 400 : { "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) : de 0 à 1. Disposition à monter. 0 évite les côtes même si le trajet s'allonge, 1 y est indifférent. Omise, Valhalla utilise sa propre valeur par défaut (0,25 sur bicycle, 0,5 sur pedestrian). Accepté sur bicycle et pedestrian. Sur auto, car ou truck, c'est un 400 : { "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"
{
  "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 : une LineString GeoJSON 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 sur code, 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."
    }
  ]
}
codeSignificationQue faire
ENDPOINT_RELOCATEDL'itinéraire ne part pas du point envoyé ou n'y arrive pas. where précise l'extrémité et le message porte la distanceConsidé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_CHECKEDL'itinéraire est revenu sans tracé exploitable : la vérification n'a pas pu s'exécuterNe l'interprétez pas comme « les extrémités sont correctes »
CLOSURE_IN_FORCE_NEAR_ROUTEUne autorité provinciale a fermé une route à moins de 250 m du tracé, en ce momentAttendez-vous à un itinéraire erroné. Le graphe est reconstruit chaque semaine : il ne peut pas contourner une fermeture
CLOSURE_SCHEDULED_NEAR_ROUTEMême chose, mais à venir. effectiveFrom indique la dateRien, 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 unique lng,lat que prend /route.
  • minutes (obligatoire) : une liste de bandes de temps séparées par des virgules, par exemple 5,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, auto par défaut) : auto, car, bicycle, pedestrian ou truck, 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 avec avoid=ferries est 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"
résultat en direct
le code qui l’a produit
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 : toujours time ici. 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 selon contour.

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, auto par défaut) : les mêmes cinq modes que /route.
  • Profil de camion (facultatif, mode=truck seulement) : 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"}'
// 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 0 est 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 bien 0, jamais null.
  • 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 null n'apparaît jamais en pratique. Ceci corrige une version antérieure de cette page, qui affirmait qu'une cellule null signifiait « inaccessible ». En sondant l'API déployée, on a découvert que Valhalla fait échouer toute la requête sources_to_targets de la même façon qu'il fait échouer un /route invalide, 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 le max_matrix_distance propre à Valhalla, que ce déploiement ne configure pas et n'a pas encore lu. Le type (number | null)[][] et la gestion de null dans le SDK sont conservés : ils ne coûtent rien, sont corrects pour un vrai 0, et restent corrects si une future version du moteur se met à mettre null sur 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, auto par défaut) : les mêmes cinq modes que /route.
  • Profil de camion (facultatif, mode=truck seulement) : 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"}'
// 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, auto par défaut) : les mêmes cinq modes que /route.
  • Profil de camion (facultatif, mode=truck seulement) : 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"}'
// 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 le stops que 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 de order. 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 200 avec 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 422 et { "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 400 too_many_locations à la passerelle, et deux arrêts donnent un 400 invalid_location avec l'indication d'utiliser /route.
  • Toute autre défaillance en amont donne un 502 avec code: "routing_failed". Une réponse que la passerelle ne peut pas convertir en tournée valide donne aussi un 502, jamais un order inventé : 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"
ParamètreUnitéPar défautAccepté
heightmètres4,11plus de 0, jusqu'à 10
widthmètres2,6plus de 0, jusqu'à 6
lengthmètres21,64plus de 0, jusqu'à 50
weighttonnes21,77plus de 0, jusqu'à 100
axle_loadtonnes9,070 à 40
axle_countnombre5entier, 2 à 20
hazmatbooléenfalsetrue ou false
use_truck_routepréférence00 (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 maxheight sur 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 maxweight et environ 500 une charge par essieu. weight et axle_load sont 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, donc axle_count n'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_route suit les tronçons hgv=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 utilisez 1, 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, et 0 n'é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 : true seulement si height, width, length, weight et axle_load ont tous été fournis. axle_count n'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é de coverage et 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 sur code, qui est stable; message est 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.

ValeurSignification
authoritativeVérifié auprès d'une source gouvernementale pour cette classe
osm_plus_authoritativeUne source gouvernementale couvre une partie du territoire, OpenStreetMap le reste
osm_onlyDonnées cartographiées par la communauté seulement. C'est une provenance, pas une note
not_modelledLa règle existe dans le monde réel, mais le moteur ne peut pas l'exprimer
not_checkedRien 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.

CodeQuandQuoi faire
INCOMPLETE_VEHICLE_PROFILEUne dimension pouvant exclure une route n'a pas été fournieEnvoyez le profil complet. Les valeurs par défaut décrivent un autre camion que le vôtre
ENDPOINT_RESTRICTIONS_UNCHECKEDLe 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 restreintVérifiez séparément le dégagement et la charge à votre origine et à votre destination
WEIGHT_DATA_SPARSEVous avez envoyé weight ou axle_loadNe lisez pas un itinéraire sans exclusion de poids comme un itinéraire homologué
HAZMAT_CLASSES_NOT_MODELLEDVous avez envoyé hazmat=trueAppliquez 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"

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 :

DemandePourquoi ce n'est pas un paramètre
Routes de ressource / industrielles / de bailL'accès est autorisé et souvent saisonnier. Un track / service OSM n'est pas un graphe de baux.
Routes d'hiver, de glace, saisonnièresOuvert/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 fermeLes 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"

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.

valeurce qu'elle pénalisemodes
tollsroutes et ponts à péageauto, car, truck
highwaysautoroutes et routes nationalesauto, car, truck
ferriestraversées en traversierles 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"

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"
  }
}
  • status vaut ready ou unavailable. L'appel renvoie 200 dans les deux cas : un point de terminaison d'état qui échoue quand le routage est en panne n'apprend rien qu'un /route en échec n'ait déjà dit.
  • graph est 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.
  • ageHours est le nombre d'heures entières depuis builtAt, arrondi vers le bas.
  • osmTimestamp est 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=auto réussit là où la même paire en mode=pedestrian retourne un 422.
  • Sur /matrix, la même lacune de couverture apparaît comme un 422 pour toute la requête, exactement comme sur /route, plutôt que comme une cellule null pour la seule paire touchée. Voir « Matrix » plus haut. /optimized-route devrait é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

  • mode atteint 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, auto retourne 991 secondes et pedestrian en 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'un 400 portant 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 } avec Retry-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 /route et /isochrone. Sur ces deux-là, private signifie que votre propre client peut réutiliser une réponse GET pendant 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, /match et /optimized-route sont des POST, et selon la RFC 9111 une réponse mise en cache pour un POST ne peut satisfaire qu'un GET ultérieur sur la même URI, jamais un autre POST : 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 routing dans 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() et match() 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.
  • mode est typé 'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck' | 'transit' dans le @unmap/routing publié, si bien qu'une faute de frappe est attrapée à la compilation autant que par le 400 de la passerelle.
  • useHills devient use_hills sur le fil, comme useHighways devient use_highways.
  • avoidAreas devient avoid_areas sur le fil, et le passer fait basculer route() et isochrone() de GET à POST. Mêmes paramètres, même réponse.
  • avoid est 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 avec status: "unavailable" au lieu d'échouer quand le routage est en panne.
  • Le profil de camion est un objet truck aux champs en camelCase (axleLoad, axleCount, useTruckRoute, plus height, width, length, weight, hazmat); le client les convertit en champs snake_case attendus par la passerelle, dans la chaîne de requête pour route/ isochrone et dans le corps JSON pour matrix/match/optimize.
  • useHighways est le même drapeau de 0 à 1 que use_highways sur le fil. Omis, le client n'envoie rien, et Valhalla garde sa valeur par défaut.
  • minutes est obligatoire sur isochrone, sans valeur par défaut. L'objet d'options des quatre autres méthodes est entièrement facultatif.
  • Les cinq méthodes acceptent un AbortSignal sous signal.
  • Une réponse non 2xx lève une RouterError qui porte le statut HTTP dans .status, et le code lisible par machine de la passerelle dans .code lorsque le corps en portait un, pour n'importe laquelle des cinq méthodes.
  • route() et match() retournent un RouteResult entièrement typé. matrix() retourne un MatrixResult (durations et distances, chacun (number | null)[][]). optimize() retourne un OptimizedRouteResult, un RouteResult plus order: number[]. isochrone() retourne une FeatureCollection volontairement minimale, dont les features sont des unknown[] : 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.