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", "code": "invalid_mode" }.
  • 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", "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) : 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", "code": "invalid_route_option" }. Voir « Dénivelé » plus bas pour savoir pourquoi la conduite est exclue.
  • steps (facultatif, false par défaut) : true ajoute steps, les indications écrites virage par virage. Voir Indications virage par virage.
  • lang (facultatif, seulement avec steps=true) : en ou fr, la langue des indications. Sans ce paramètre, la passerelle lit Accept-Language, puis se rabat sur en.
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. 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"

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
}
  • type est 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 commun transit (monter), transit_transfer, transit_stay_on, station_enter, station_transfer, station_exit, et les étapes de marche elevator, stairs, escalator, building_enter, building_exit. other est une étape que le moteur a produite et qu'unmap ne nomme pas encore; affichez son instruction.
  • modifier vaut sharp_left, left, slight_left, straight, slight_right, right ou sharp_right. Sur depart et arrive, c'est le côté de la route; sur uturn, le sens du demi-tour.
  • geometryRange est inclusif. geometry.coordinates.slice(début, fin + 1) est le tracé à mettre en évidence quand on survole l'indication, et location en est le premier point.
  • roundaboutExit (sur roundabout) indique quelle sortie prendre, en comptant depuis l'entrée.
  • sign reprend un panneau d'autoroute tel qu'affiché : exitNumbers, branches (la route sur laquelle mène la sortie), toward et names. 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 forme lng,lat, l'écriture qu'utilisent aussi /places/nearby, /transit/stops et /incidents.
  • 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, truck ou transit, validé exactement comme sur /route, et acceptant les mêmes paramètres de profil de camion.
  • depart_at (mode=transit seulement) : 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 avec avoid=ferries est 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"
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.

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, 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 : 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"}'
// 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.
  • 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 à null sur place ; la ligne garde sa longueur et chaque autre cellule garde son sens, donc durations[i][j] correspond toujours à l'origine i vers la destination j. Gérez null sur 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 vrai 0 sur la diagonale. Ceci corrige une version antérieure de cette page, qui affirmait qu'une cellule null n'apparaissait jamais.
  • N'affichez pas un null comme « 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, chaque null correspondait à une paire que /route calcule avec une vraie distance routière, y compris les cellules Moosonee ci-dessus. Traitez null comme « pas de réponse pour cette paire », et repliez-vous sur /route si 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, 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.
  • steps et lang (facultatifs) : les mêmes indications virage par virage que /route, avec steps en 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"}'
// 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, 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.
  • steps et lang (facultatifs) : les mêmes indications virage par virage que /route, avec steps en 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"}'
// 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: "upstream_error". 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.

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"
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 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 : 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 (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"

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

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"
  }
}
  • 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. 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 un 400 invalid_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à est no_route. La lecture du 2026-09-02 a consigné ce cas comme un 422.
  • Être au Canada n'est pas la même chose qu'être relié par la route. Calgary vers Iqaluit donne un 422 no_route, puisqu'Iqaluit n'a aucun lien routier avec le continent. Un itinéraire dans Iqaluit fonctionne. Le 422 de 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=auto réussit là où la même paire en mode=pedestrian retourne un 422.
  • Sur /matrix, une paire que le moteur ne parvient pas à relier apparaît comme une cellule null, 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 un 422 no_route, car cette vérification a lieu avant qu'une seule cellule existe. Un point hors du Canada est outside_coverage avant l'une ou l'autre de ces réponses. Voir « Matrix » plus haut. /optimized-route n'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 un null dans la matrice sous-jacente ne suffit pas à la faire échouer.

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 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() 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.
  • router.optimizeTours(request) est Unmap Optimize, pas l'une des cinq : elle prend un OptimizeToursRequest et renvoie { response, warnings, billableShipments, timeoutClamped, clampedTimeoutSeconds }, et un échec lève UnmapOptimizeError. Elle est offerte depuis la 0.21.0. Voir API Optimize.
  • 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.