{"openapi":"3.0.1","info":{"title":"spring-org API","description":"API **100 % locale** (aucune dependance a une API externe a l'execution) reunissant trois briques :\n\n1. **Routing** (`/routing`) — calcul d'itineraire et de matrices de temps via GraphHopper embarque, a partir d'un extrait OpenStreetMap de la France.\n2. **Optimisation** (`/optimization`) — resolution de tournees via Timefold : `/optimize` = ordre de passage optimal de N points (TSP/VRP/VRPTW, minimise le temps total) ; `/dispatch` = **repartition automatique** de N points (une centaine ou plus) en K tournees **equilibrees en duree** (zones geographiques coherentes, chaque vehicule utilise), sans s'appuyer sur la capacite ; temps de resolution choisi par requete. Les deux acceptent en OPTION des **missions appairees** (`shipments` : chargement a un endroit -> enlevement a un autre), servies par le meme vehicule et dans cet ordre.\n3. **Geocoding** (`/geocoding`) — recherche / autocompletion d'adresses, de voies et de communes (Base Adresse Nationale, BAN) et de **points d'interet** (commerces, enseignes, services publics, gares... extraits du meme fichier OpenStreetMap que le routing), via des index Lucene locaux.\n\n### Donnees\nLes fichiers (OSM ~5 Go, BAN ~900 Mo) sont **telecharges automatiquement** au demarrage s'ils manquent. Ils sont ensuite **mis a jour automatiquement** par un pipeline commun : **(1) a chaque demarrage**, une fois les moteurs charges avec les donnees existantes (l'API reste servie pendant ce temps), si la source publie une version plus recente (requete HEAD, comparaison `Last-Modified` avec la version locale) ; **(2) tous les dimanches a 8h** (heure de Paris). Une mise a jour re-telecharge le fichier, reconstruit le graphe / les index et bascule a chaud. Pendant la reconstruction du graphe routier, `/routing` et `/optimization` renvoient **503** ; le geocoding reste disponible pendant la reconstruction de ses index. Chaque execution produit un **compte rendu** (declencheur, machine, versions publiees avant/apres, tailles, noeuds/aretes du graphe, adresses indexees, durees) publie sur le webhook Discord configure et resume dans `GET /status` (objet `dataUpdate`). Pendant qu'une brique n'a pas ses donnees, ses endpoints renvoient **503** (corps `application/problem+json`) ; consulter `GET /status` de chaque brique.\n\n### Codes de reponse transverses\n- `200` succes\n- `400` requete invalide (validation des champs, parametre non numerique, coordonnees hors bornes)\n- `422` requete correcte mais sans solution (point en mer, aucune route entre deux points) : inutile de reessayer\n- `503` sous-systeme indisponible (donnees absentes / en cours de (re)construction)\n","contact":{"name":"StackBZH"},"license":{"name":"Proprietaire"},"version":"0.4.0"},"servers":[{"url":"http://ors.stack.bzh","description":"Generated server url"}],"tags":[{"name":"Routing","description":"Calcul d'itineraires et de matrices de temps via GraphHopper embarque (extrait OpenStreetMap de la France). Necessite que le graphe routier soit charge (voir GET /routing/status). Sinon : 503."},{"name":"Optimisation","description":"Optimisation de tournees via Timefold, avec fenetres horaires (VRPTW). Deux operations :\n- **`/optimize`** : ordre de passage optimal d'une liste de points (TSP/VRP) en minimisant le temps total (conduite + attente). Avec plusieurs vehicules sans capacite, le solveur tend a n'en utiliser qu'un seul.\n- **`/dispatch`** : repartition AUTOMATIQUE de N points (une centaine ou plus) en K tournees EQUILIBREES en duree, chaque tournee couvrant une zone geographique coherente et etant ordonnee. Ne repose pas sur la capacite ; temps de resolution choisi par requete.\n\nLes deux acceptent, EN OPTION et melangeables avec les points simples `visits`, des **missions appairees `shipments`** (chargement a un endroit -> enlevement a un autre) : l'API garantit alors que les deux arrets d'une mission sont servis par le MEME vehicule et que le chargement passe AVANT l'enlevement. Une requete sans `shipments` se comporte exactement comme avant.\n\nLes deux s'appuient sur le moteur de routing pour la matrice de temps reels : si le routing est indisponible, elles renvoient 503."},{"name":"Geocoding","description":"Recherche / autocompletion d'adresses (Base Adresse Nationale), de voies, de communes et de points d'interet (OpenStreetMap : commerces, services publics, gares, sites touristiques...) via des index Lucene locaux. Tolerant aux accents, aux apostrophes, aux abreviations (`st`, `bd`, `av`...), aux mots vides et aux fautes de frappe ; le dernier mot saisi est complete pour l'autocompletion. Necessite l'index des adresses charge (voir GET /geocoding/status), sinon 503. Les points d'interet sont facultatifs : tant que leur index n'est pas charge, la recherche fonctionne sans eux."},{"name":"Statut","description":"Etat de l'application : sous-systemes, telechargements, memoire JVM, donnees, mise a jour automatique des donnees (derniere execution, prochaine planifiee)."}],"paths":{"/routing/route":{"post":{"tags":["Routing"],"summary":"Itineraire point a point ou multi-points ordonne","description":"Calcule un itineraire routier. Deux modes (cf. corps de requete) :\n\n- **point a point** : fournir `from` et `to` ;\n- **multi-points ordonne** : fournir `points` (>= 2 elements). L'itineraire passe par tous les points DANS L'ORDRE FOURNI (aucune reoptimisation : pour reordonner, utiliser POST /optimization/optimize). Chaque paire consecutive est routee, puis les troncons sont concatenes en une trace continue (sans point en double aux jonctions).\n\nRetour : `distanceMeters` et `durationSeconds` cumules sur tout le parcours, plus la geometrie. `geometry` (liste [lat,lon]) est renseigne si `geometryFormat=POINTS` ; `geometryPolyline` (encodage Google/OSRM, precision 5, ordre lat/lon) est renseigne pour POINTS comme POLYLINE (null seulement si NONE).","operationId":"route","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteRequest"}}},"required":true},"responses":{"400":{"description":"Corps invalide : ni (`from`+`to`) ni `points` (>= 2) fournis, ou coordonnees hors bornes WGS84"},"422":{"description":"Un point est hors du reseau routier (en mer, hors zone couverte), ou deux points ne sont relies par aucune route : la reponse le designe"},"503":{"description":"Moteur de routing indisponible (graphe non charge)"},"200":{"description":"Itineraire calcule","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RouteResponse"}}}}}}},"/routing/matrix":{"post":{"tags":["Routing"],"summary":"Matrice des temps de trajet","description":"Calcule la matrice NxN des temps de trajet (secondes) entre tous les points fournis.\n`durationsSeconds[i][j]` = temps pour aller du point i au point j (peut etre asymetrique a cause des sens uniques ; la diagonale vaut 0).\nUtilise en amont d'une optimisation. Cout : N*N routages (rapide grace aux Contraction Hierarchies). Le nombre de points est plafonne par `app.routing.max-matrix-points` (defaut 1000).","operationId":"matrix","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixRequest"}}},"required":true},"responses":{"400":{"description":"Liste de points vide ou invalide (coordonnees hors bornes WGS84), ou plus de points que le plafond configure"},"200":{"description":"Matrice calculee","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MatrixResponse"}}}},"503":{"description":"Moteur de routing indisponible"},"422":{"description":"Un point est hors du reseau routier (en mer, hors zone couverte), ou deux points ne sont relies par aucune route : la reponse le designe"}}}},"/optimization/optimize":{"post":{"tags":["Optimisation"],"summary":"Optimiser une tournee (ordre de passage, fenetres horaires, heure de depart)","description":"Optimise l'ordre de passage d'une liste de points depuis/vers un depot commun.\n\nDeroulement interne :\n1. construction des points (depot + visites) ; les fenetres horaires sont converties en offsets (secondes) par rapport a `departureTime` ;\n2. calcul de la matrice des temps reels via le routing local (GraphHopper) ;\n3. resolution du VRP avec Timefold (duree allouee = `timefold.solver.termination.spent-limit`, 1 s par defaut) : contraintes DURES = capacite vehicule et arrivee avant `timeWindowEnd` ; contraintes SOUPLES (minimisees) = temps de conduite total + temps d'attente aux fenetres ;\n4. renvoi des tournees ordonnees par vehicule, avec les heures d'arrivee/debut de service/depart de chaque arret.\n\nCas d'usage typiques :\n- **1 vehicule, capacite illimitee** : simple optimisation d'ordre (TSP). Omettre `vehicleCapacity` (= illimite, aucune contrainte de charge) et `demand` (= 0). L'ordre optimal de passage est `routes[0].stops[].visitId` dans l'ordre du tableau.\n- **N vehicules + `vehicleCapacity` + `demand`** : repartition capacitaire (CVRP).\n- **Fenetres horaires (VRPTW)** : ex. une pharmacie a livrer entre 21h et 23h -> `departureTime: \"2026-06-15T20:00:00\"` sur la requete et, sur la visite, `timeWindowStart: \"2026-06-15T21:00:00\"`, `timeWindowEnd: \"2026-06-15T23:00:00\"`. Les autres visites sans fenetre (= « pas d'importance ») sont placees librement autour.\n\n**Heure de depart estimee (`departureTime`)** : origine des temps de la tournee. Toutes les heures de la reponse en decoulent et les fenetres horaires sont evaluees par rapport a elle. Omise = heure courante du serveur. A TOUJOURS renseigner quand des fenetres sont fournies.\n\n**Semantique des fenetres horaires** (chaque borne est optionnelle ; aucune = pas de contrainte) :\n- `timeWindowStart` : si le vehicule arrive avant, il **attend** sur place jusqu'a l'ouverture (`stops[].waitingSeconds`, `serviceStartTime` = ouverture). L'attente est minimisee (soft) et toleree jusqu'a **`maxWaitingSeconds`** (requete ; defaut serveur `app.optimization.max-waiting-seconds` = 3600 s = 1 h ; `0` = illimite). Au-dela, l'arret est considere comme NE CORRESPONDANT PAS au creneau (contrainte dure) : le solveur reordonne pour l'eviter et, si aucun ordre ne le permet, la tournee est renvoyee avec `feasible=false`, `stops[].timeWindowStatus=WAITING_TOO_LONG` et `stops[].excessiveWaitingSeconds` > 0 (les heures restent calculees comme si le vehicule attendait l'ouverture). Cela evite les tournees ou le vehicule attend des heures devant un client ferme.\n- `timeWindowEnd` : heure LIMITE d'**arrivee** (contrainte dure). Le solveur reordonne les points pour la tenir. Si c'est impossible (fenetre deja passee, trop de points, attentes en chaine), la tournee est quand meme renvoyee (meilleure solution trouvee) avec `feasible=false`, `timeWindowViolations` > 0, `stops[].timeWindowStatus=LATE` et `stops[].lateSeconds` > 0 sur les arrets concernes ; le score `hard` est negatif. Aucune notification Discord n'est emise (resultat metier normal). Ce n'est PAS un 400 : le client doit lire `feasible` et decider (accepter le retard, changer `departureTime`, retirer un point).\n- `timeWindowStart` > `timeWindowEnd` -> 400 (validation).\n\nChaque arret expose `timeWindowStatus` : `OK`, `LATE` ou `WAITING_TOO_LONG` ; tout statut autre que `OK` compte dans `timeWindowViolations` et rend `feasible=false`. La reponse rappelle la limite appliquee dans `maxWaitingSeconds` (null = pas de limite).\n\nSemantique des autres valeurs par defaut : `vehicleCount` omis = 1 ; `vehicleCapacity` omis = illimite ; `demand` omis = 0 ; `serviceDurationSeconds` omis = 0 ; `maxWaitingSeconds` omis = defaut serveur (3600 s). L'ordre des arrets retourne EST l'ordre de passage optimal.\n\nNote : avec capacite illimitee et plusieurs vehicules, le solveur tend a n'en utiliser qu'un (chaque vehicule ajoute un aller-retour au depot), sauf si les fenetres horaires l'imposent. Pour repartir N points en K tournees equilibrees en duree, utiliser **`/optimization/dispatch`**.\n\nLa reponse est enrichie pour l'affichage : pour chaque segment (point precedent -> point), distance (m), duree (s) et **geometrie** ; pour chaque arret, distance/temps cumules, **`arrivalTime`, `serviceStartTime`, `departureTime`**, la fenetre demandee, `waitingSeconds`, `excessiveWaitingSeconds`, `lateSeconds` et `timeWindowStatus`. Par tournee : `departureTime`, `returnTime`, temps de conduite/service/attente. Chaque `routes[]` expose aussi **`geometry`/`geometryPolyline`** : la trace COMPLETE de la tournee (depot -> arrets -> depot), a utiliser pour afficher le trace global d'un seul trait (les polylignes par segment ne se concatenent pas). Mettre `geometryFormat=NONE` (ou `includeGeometry=false`) pour alleger la reponse.\n\nPoints non rattachables au reseau routier (tolerance) : avant l'optimisation, chaque point est teste contre le reseau routier. Une visite dont les coordonnees ne peuvent PAS etre rattachees a une route (en mer, hors zone OSM couverte, reseau deconnecte) ou dont la route la plus proche depasse le seuil `app.routing.max-snap-distance-meters` (1000 m par defaut) est **ECARTEE** de l'optimisation : elle n'apparait dans aucune tournee et est listee dans **`skippedVisits[]`** (avec `reason` = `UNROUTABLE` ou `TOO_FAR` et la distance de rattachement). Une seule visite invalide ne fait donc PLUS echouer toute la requete (plus de 503 pour ce motif) : la tournee est calculee avec les points valides restants. Le client DOIT inspecter `skippedVisits` et signaler/corriger ces points. Si TOUTES les visites sont ecartees, la reponse est un 200 avec `routes` vide et tous les points dans `skippedVisits`. En revanche, si c'est le **depot** qui n'est pas rattachable, l'optimisation est impossible -> **400**.\n\n**Missions appairees (`shipments`) — OPTION.** A cote de `visits` (des points independants, que le solveur ordonne librement), la requete peut porter des `shipments` : une marchandise chargee a un endroit (`pickup`) puis enlevee/deposee a un autre (`delivery`). L'API ajoute alors deux contraintes DURES : les deux arrets sont servis par le **MEME vehicule**, et le **chargement passe AVANT l'enlevement**. Les deux listes se melangent librement ; une requete sans `shipments` se comporte a l'identique de l'existant.\n- Chaque extremite est un `VisitDto` complet : elle a sa propre duree de service et sa propre fenetre horaire (on peut donc exiger un chargement le matin et une depose l'apres-midi).\n- Une mission peut n'avoir qu'UNE extremite : `pickup` absent = marchandise chargee au depot au depart (livraison classique) ; `delivery` absent = marchandise ramenee au depot en fin de tournee (collecte). Le depot encadrant deja la tournee, aucun ordre n'est impose dans ce cas.\n- Dans la reponse, chaque arret porte `shipmentId` et `stopType` (`PICKUP`/`DELIVERY`) pour recoller les deux bouts ; `pairingViolations` compte les missions cassees (0 attendu).\n- **Points non rattachables** : une mission est ecartee EN ENTIER ou pas du tout. Si une seule extremite n'est pas rattachable, l'autre est ecartee avec `reason` = `PAIRED_POINT_SKIPPED` (on ne peut pas enlever une marchandise jamais chargee).\n- **Temps de resolution** : des qu'il y a des missions, le probleme est plus difficile (l'etat initial ignore la precedence, le solveur doit d'abord la reparer). Le budget passe donc de la terminaison globale (1 s) a `maxSolvingSeconds`, sinon au defaut serveur `app.optimization.shipments.default-solving-seconds` (5 s), plafonne a 60 s. La requete HTTP dure alors au moins ce temps ; la valeur appliquee est renvoyee dans `solvingTimeSeconds`.\n- `demand` garde sa semantique : une SOMME comparee a `vehicleCapacity` sur la tournee entiere, et non un suivi de la charge a bord. Le porter sur le `pickup` et laisser le `delivery` a 0, sinon la charge est comptee deux fois.\n- Les identifiants doivent etre uniques dans la requete, qu'il y ait des missions ou non (la matrice des temps et l'appairage passent par eux) -> sinon **400**.","operationId":"optimize","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OptimizeRequest"}}},"required":true},"responses":{"200":{"description":"Tournee(s) optimisee(s). Verifier `feasible` (false = au moins une fenetre horaire (retard ou attente > `maxWaitingSeconds`), une capacite non tenable ou une mission appairee cassee ; details dans `stops[].timeWindowStatus`, `lateSeconds`, `excessiveWaitingSeconds` et `pairingViolations`) et `skippedVisits` (points ecartes car non rattachables au reseau routier, trop eloignes, ou parce que l'autre extremite de leur mission l'etait).","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OptimizeResponse"}}}},"503":{"description":"Routing indisponible (matrice non calculable)"},"400":{"description":"Depot manquant/non rattachable au reseau routier, requete sans aucun point (`visits` et `shipments` vides), mission sans `pickup` ni `delivery`, identifiant de point duplique, coordonnees hors bornes, plus de 1000 vehicules ou de points, fenetre horaire invalide (`timeWindowStart` > `timeWindowEnd`), `maxWaitingSeconds` negatif ou `maxSolvingSeconds` < 1"},"422":{"description":"Deux points rattaches au reseau routier ne sont relies par aucune route (ex : ile sans liaison) : la reponse le designe"}}}},"/optimization/dispatch":{"post":{"tags":["Optimisation"],"summary":"Repartir N points en K tournees equilibrees en duree (repartition automatique)","description":"Recoit un depot, un NOMBRE de tournees `vehicleCount` (ex : 5) et une liste de points (ex : 100) et repartit elle-meme les points entre les tournees, puis ordonne chaque tournee. Aucune capacite ni aucun `demand` n'est necessaire : la repartition se fait sur les DUREES.\n\n**Difference avec `/optimize`** : `/optimize` minimise le temps total et, avec plusieurs vehicules sans capacite, finit mathematiquement par tout mettre sur un seul vehicule (chaque vehicule supplementaire ajoute un aller-retour au depot). `/dispatch` change l'objectif : il minimise la **somme des CARRES des durees de tournee** (duree = conduite + service + attentes). Cette somme vaut (temps total)^2 / K + K x variance : la minimiser reduit a la fois le temps global ET l'ecart entre vehicules, sans reglage. Resultat : les K vehicules sont utilises, chacun couvre une zone geographique coherente (les points proches finissent dans la meme tournee) et les durees sont proches sans etre strictement egales (un desequilibre est accepte s'il fait gagner du temps globalement, ou si les fenetres horaires / la geographie l'imposent). Avec `vehicleCount = 1`, l'optimum est le meme que `/optimize`.\n\nDeroulement interne :\n1. verification du depot (non rattachable -> 400) et de chaque point (non rattachable -> ecarte dans `skippedVisits`, cf. plus bas) ; fenetres horaires converties en offsets par rapport a `departureTime` ;\n2. matrice des temps reels entre tous les points (routing local GraphHopper, (N+1)^2 routages : ~10 000 pour 100 points, quelques secondes) ;\n3. resolution Timefold pendant **`maxSolvingSeconds`** (defaut serveur 10 s, plafond 60 s) : contraintes DURES = arrivee avant `timeWindowEnd`, attente <= `maxWaitingSeconds`, capacite si `vehicleCapacity` est fourni ; contrainte SOUPLE dominante = somme des carres des durees de tournee (+ conduite et attente en departage) ;\n4. reponse : une tournee ordonnee par vehicule (memes champs que `/optimize` : segments, cumuls, heures d'arrivee/debut de service/depart, statut de fenetre, geometrie complete) + indicateurs d'equilibre (`balance`).\n\n**Temps de reponse** : la requete HTTP dure au moins `maxSolvingSeconds` + le calcul de la matrice. C'est voulu : plus le budget est long, meilleure est la repartition (100 points / 5 tournees : ~10 s correct, 20-30 s tres bon). Prevoir un timeout client d'au moins 90 s. Une valeur superieure au plafond serveur est ramenee silencieusement au plafond ; le budget reellement applique est renvoye dans `solvingTimeSeconds`. Chaque resolution occupe un thread du solveur pendant toute sa duree : les appels simultanes au-dela du parallelisme configure sont mis en file d'attente.\n\n**Fenetres horaires, attente max, heure de depart** : memes regles et memes champs que `/optimize` (`departureTime` = origine des temps, `timeWindowStart` -> attente, `timeWindowEnd` -> heure limite d'arrivee (dure), `maxWaitingSeconds` -> attente toleree). Une fenetre intenable n'est pas un 400 : la meilleure repartition est renvoyee avec `feasible=false`, `timeWindowViolations` > 0 et `stops[].timeWindowStatus` = `LATE` ou `WAITING_TOO_LONG`. Toutes les tournees partent du depot a `departureTime`.\n\n**Lecture de la reponse** : `routes` contient exactement `vehicleCount` entrees (`vehicle-0` .. `vehicle-<K-1>`), chacune deja ordonnee (`stops[]` = ordre de passage) avec sa duree totale `durationSeconds`. `usedVehicleCount` < `vehicleCount` seulement s'il y a moins de points valides que de vehicules (tournees vides). `balance` donne la tournee la plus longue / la plus courte, la moyenne et l'ecart entre tournees utilisees. `totalDurationSeconds` = somme des durees. Le champ `score` n'est comparable qu'entre deux resolutions du meme probleme.\n\n**Capacite (facultative)** : si `vehicleCapacity` est fourni, la somme des `demand` d'une tournee ne peut pas le depasser (contrainte dure, `feasible=false` si intenable). Sans `vehicleCapacity`, `demand` est ignore.\n\n**Points non rattachables** : meme tolerance que `/optimize`. Une visite en mer / hors zone / a plus de `app.routing.max-snap-distance-meters` de toute route est ECARTEE et listee dans `skippedVisits[]` (`reason` = `UNROUTABLE` ou `TOO_FAR`) ; la repartition se fait sur les points restants. Toutes ecartees -> 200 avec `routes` vide et `usedVehicleCount = 0`. Depot non rattachable -> 400.\n\n**Geometrie** : `geometryFormat` = `POINTS` (defaut), `POLYLINE` (recommande : une centaine de segments) ou `NONE`. Par tournee, `geometry`/`geometryPolyline` = trace COMPLETE depot -> arrets -> depot (polyline remplie sauf en NONE) ; par arret, `legFromPrevious` = segment individuel.\n\n**Missions appairees (`shipments`) — OPTION.** A cote de `visits`, la requete peut porter des missions « chargement -> enlevement » (memes champs et memes regles que sur `/optimize`). Une mission n'est JAMAIS coupee entre deux tournees : ses deux arrets partent dans la meme, le chargement d'abord (contraintes DURES). L'equilibrage porte donc sur des missions entieres, ce qui reduit un peu la finesse de la repartition : a nombre de points egal, l'ecart `balance.spreadSeconds` est generalement plus grand qu'avec des points independants, et un budget `maxSolvingSeconds` plus large est recommande (le solveur doit d'abord reparer la precedence avant d'optimiser). Chaque arret de la reponse porte `shipmentId` et `stopType` (`PICKUP`/`DELIVERY`) ; `pairingViolations` compte les missions cassees (0 attendu). Si une extremite n'est pas rattachable au reseau, la mission entiere est ecartee (`reason` = `PAIRED_POINT_SKIPPED`). Une requete sans `shipments` se comporte a l'identique de l'existant.\n\nExemple minimal : `{ \"depot\": {\"lat\": 48.1173, \"lon\": -1.6778}, \"vehicleCount\": 5, \"maxSolvingSeconds\": 20, \"geometryFormat\": \"POLYLINE\", \"visits\": [ {\"id\": \"A\", \"lat\": 48.12, \"lon\": -1.70}, ... 100 points ... ] }`.","operationId":"dispatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DispatchRequest"}}},"required":true},"responses":{"200":{"description":"Repartition calculee : `routes` (une tournee ordonnee par vehicule, `vehicleCount` entrees), `balance` (equilibre des durees), `solvingTimeSeconds` (budget applique). Verifier `feasible` (false = fenetre horaire, attente max, capacite intenable ou mission appairee cassee ; details dans `stops[].timeWindowStatus` et `pairingViolations`) et `skippedVisits` (points ecartes).","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DispatchResponse"}}}},"400":{"description":"Depot manquant ou non rattachable au reseau routier, `vehicleCount` absent ou < 1, requete sans aucun point (`visits` et `shipments` vides), mission sans `pickup` ni `delivery`, identifiant de point duplique, coordonnees hors bornes, plus de 1000 vehicules ou de points, coordonnee manquante, fenetre horaire invalide (`timeWindowStart` > `timeWindowEnd`), `maxWaitingSeconds` negatif ou `maxSolvingSeconds` < 1"},"503":{"description":"Routing indisponible (matrice non calculable : donnees absentes ou graphe en cours de (re)construction)"},"422":{"description":"Deux points rattaches au reseau routier ne sont relies par aucune route (ex : ile sans liaison) : la reponse le designe"}}}},"/status":{"get":{"tags":["Statut"],"summary":"Etat complet de l'API","description":"Statut global, etat de chaque sous-systeme, telechargements en cours, memoire JVM, presence/taille des fichiers de donnees et objet `dataUpdate` (configuration de la mise a jour automatique des donnees, prochaine execution planifiee, derniere execution avec son declencheur, son etat et un resume par jeu de donnees). Le tableau de bord '/' interroge cet endpoint toutes les 2 s. Toujours 200, meme quand des briques sont indisponibles (voir `status` et `components`).","operationId":"status","responses":{"200":{"description":"Etat courant de l'API.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/routing/status":{"get":{"tags":["Routing"],"summary":"Etat du moteur de routing","description":"Indique si le graphe routier est charge (`ready=true`) et le profil utilise (`car`). A interroger avant tout appel de routing : si `ready=false`, les autres endpoints renvoient 503.","operationId":"status_1","responses":{"200":{"description":"Etat retourne","content":{"*/*":{"schema":{"type":"string","example":{"ready":true,"profile":"car"}}}}}}}},"/geocoding/status":{"get":{"tags":["Geocoding"],"summary":"Etat des index de recherche","description":"Indique si l'index des adresses (`ready`) et celui des points d'interet (`poiReady`) sont charges, et leur taille. Seul `ready=false` provoque des 503 sur `/search`.","operationId":"status_2","responses":{"200":{"description":"Etat retourne","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GeocodingStatus"}}}}}}},"/geocoding/search":{"get":{"tags":["Geocoding"],"summary":"Rechercher une adresse, une voie, une commune ou un point d'interet","description":"Recherche plein texte avec autocompletion. Renvoie les resultats les plus pertinents (tries par score decroissant), avec leurs coordonnees GPS. Tous les types sont melanges par defaut ; `types` restreint la recherche.\n\n**Nature des resultats** (`type`) :\n- `housenumber` : adresse precise. Uniquement si la requete contient un numero de voie, en tete (`12 bis rue du bocage`) ou en fin (`rue du bocage 12`), et que ce numero existe. Un numero compte de 1 a 4 chiffres, avec une lettre ou un indice eventuels ; un code postal n'en est pas un. Une voie dont le nom contient des nombres reste trouvable (`rue du 8 mai 1945`).\n- `street` : voie, une entree par rue distincte (commune + code postal), sans numero. Si le numero demande n'existe pas, la voie est proposee a la place.\n- `municipality` : commune, une entree par commune meme si elle a plusieurs codes postaux. `35000` ou `rennes` la trouvent.\n- `poi` : point d'interet OpenStreetMap, avec `name`, `category`, `categoryLabel`, `brand`. Cherchable par nom (`tour eiffel`), enseigne (`carrefour`), type (`boulangerie`, `pharmacie`, `essence`), le tout combinable avec une commune (`carrefour rennes`) grace au rattachement de chaque POI a la commune de la voie la plus proche. Seuls les objets qui ont un nom ou une enseigne sont connus : une boulangerie anonyme dans OpenStreetMap n'est pas trouvable.\n\n**Ecriture tolerante** — casse, accents, ligatures, apostrophes et tirets sont ignores ; les mots vides (`de`, `la`, `du`...) ne sont pas exiges ; les abreviations `st`, `ste`, `av`, `bd`, `pl`, `rte`, `che`, `imp`, `dr`, `gal`, `mal`, `pdt`, `hop`... valent le mot complet ; les pluriels simples sont ignores. Le dernier mot est complete (`rue de la paix ren`).\n\n**Fautes de frappe** — si un mot est tres rare dans l'index, ses voisins orthographiques sont aussi cherches (`carefour` trouve `Carrefour`), classes apres les correspondances exactes. Si rien n'est trouve, la recherche se relache progressivement : sans le numero, puis avec correction de tous les mots de 4 lettres ou plus, puis en tolerant un mot manquant.\n\n**Classement** — un nom entierement couvert par la requete passe devant une correspondance partielle (`carrefour` : l'enseigne avant `Rue du Carrefour` ; `rue du bocage` : `Rue du Bocage` avant `Rue du Parc du Bocage`).\n\n**Biais de proximite (optionnel)** — si `lat` et `lon` sont fournis, les resultats proches sont favorises et chaque resultat expose `distanceMeters`. Exception : les communes ne sont pas penalisees par la distance (chercher `paris` depuis Rennes doit trouver Paris). Sans position, le classement est purement textuel et `distanceMeters` vaut `null`. Indispensable pour les recherches generiques (`boulangerie`, `carrefour`) : sans position, les resultats peuvent etre n'importe ou en France.\n\n**Effets de bord** : aucun (lecture seule). Une erreur d'index renvoie 500.\n\nExemples :\n- `q=rennes` -> la commune de Rennes en tete\n- `q=rue du bocage&lat=48.11&lon=-1.68` -> les rues du Bocage les plus proches d'abord\n- `q=12 bis rue de la paix ren` -> adresse precise, dernier mot en cours de saisie\n- `q=carrefour rennes` -> les magasins Carrefour de Rennes\n- `q=boulangerie&lat=48.11&lon=-1.68&types=poi` -> les boulangeries les plus proches","operationId":"search","parameters":[{"name":"q","in":"query","description":"Texte recherche : adresse, rue, commune, code postal, nom de lieu, enseigne, type de commerce. L'autocompletion s'applique au dernier mot.","required":true,"schema":{"type":"string"},"example":"carrefour rennes"},{"name":"limit","in":"query","description":"Nombre maximum de resultats. Ramene silencieusement dans l'intervalle 1 a 50.","required":false,"schema":{"type":"integer","format":"int32","default":10},"example":10},{"name":"lat","in":"query","description":"Latitude WGS84 de la position de reference (optionnel). Si fournie, `lon` doit l'etre aussi.","required":false,"schema":{"type":"number","format":"double"},"example":48.1147},{"name":"lon","in":"query","description":"Longitude WGS84 de la position de reference (optionnel). Si fournie, `lat` doit l'etre aussi.","required":false,"schema":{"type":"number","format":"double"},"example":-1.6794},{"name":"types","in":"query","description":"Types de resultats acceptes, separes par des virgules : `housenumber`, `street`, `municipality`, `poi`. Absent = tous. Ex : `types=poi` pour ne chercher que des lieux, `types=housenumber,street` pour retrouver un comportement purement postal.","required":false,"schema":{"type":"array","items":{"type":"string"}},"example":"poi,street"}],"responses":{"400":{"description":"Parametres invalides : `q` absent, `lat` sans `lon`, coordonnees hors bornes, `limit`/`lat`/`lon` non numeriques, valeur de `types` inconnue"},"200":{"description":"Liste de resultats, triee par pertinence (peut etre vide ; `q` sans aucun mot exploitable donne une liste vide)","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AddressResult"}}}}},"503":{"description":"Index des adresses indisponible (en construction ou fichier BAN manquant)"}}}}},"components":{"schemas":{"Coordinate":{"required":["lat","lon"],"type":"object","properties":{"lat":{"maximum":90,"exclusiveMaximum":false,"minimum":-90,"exclusiveMinimum":false,"type":"number","description":"Latitude en degres decimaux, dans [-90, 90].","format":"double","example":48.1173},"lon":{"maximum":180,"exclusiveMaximum":false,"minimum":-180,"exclusiveMinimum":false,"type":"number","description":"Longitude en degres decimaux, dans [-180, 180].","format":"double","example":-1.6778}},"description":"Point geographique en coordonnees WGS84.","nullable":true},"RouteRequest":{"type":"object","properties":{"from":{"$ref":"#/components/schemas/Coordinate"},"to":{"$ref":"#/components/schemas/Coordinate"},"points":{"type":"array","description":"Liste ordonnee des points de passage (mode multi-points). >= 2 elements. L'ordre est respecte tel quel ; distance/duree sont cumulees et la geometrie est une trace continue de bout en bout. Prioritaire sur `from`/`to` si fournie.","nullable":true,"items":{"$ref":"#/components/schemas/Coordinate"}},"geometryFormat":{"type":"string","description":"Format de la geometrie :\n- POINTS   : liste de points [lat,lon] (champ `geometry`). Lisible mais volumineux.\n- POLYLINE : chaine encodee (champ `geometryPolyline`), algorithme Google/OSRM precision 5 (~1 m), bien plus compacte pour les longs trajets. Decodable avec @mapbox/polyline, etc.\n- NONE     : pas de geometrie (reponse la plus legere).","nullable":true,"default":"POINTS","enum":["POINTS","POLYLINE","NONE"]}},"description":"Demande d'itineraire. Deux modes mutuellement exclusifs :\n- point a point : renseigner `from` et `to` ;\n- multi-points ordonne : renseigner `points` (>= 2 elements). L'itineraire passe par tous les points DANS L'ORDRE FOURNI (aucune reoptimisation ; pour reordonner, utiliser /optimization/optimize). Si `points` est present, il prime sur `from`/`to`."},"RouteResponse":{"type":"object","properties":{"distanceMeters":{"type":"number","description":"Distance totale en metres.","format":"double","example":104532.0},"durationSeconds":{"type":"integer","description":"Duree totale en secondes.","format":"int64","example":4380},"geometryFormat":{"type":"string","description":"Format de geometrie demande : POINTS, POLYLINE ou NONE. Rappel : `geometryPolyline` est renseigne pour POINTS et POLYLINE.","example":"POINTS"},"geometry":{"type":"array","description":"Trace en liste de points [latitude, longitude]. Non nul uniquement si format = POINTS. Sur un trajet multi-points, c'est la trace continue de bout en bout (sans doublon aux jonctions).","nullable":true,"example":[[48.1173,-1.6778],[48.0,-1.7],[47.2184,-1.5536]],"items":{"type":"array","description":"Trace en liste de points [latitude, longitude]. Non nul uniquement si format = POINTS. Sur un trajet multi-points, c'est la trace continue de bout en bout (sans doublon aux jonctions).","nullable":true,"example":[[48.1173,-1.6778],[48.0,-1.7],[47.2184,-1.5536]],"items":{"type":"number","description":"Trace en liste de points [latitude, longitude]. Non nul uniquement si format = POINTS. Sur un trajet multi-points, c'est la trace continue de bout en bout (sans doublon aux jonctions).","format":"double","nullable":true}}},"geometryPolyline":{"type":"string","description":"Trace en polyligne encodee. Renseigne (non nul) pour les formats POINTS et POLYLINE ; null uniquement si format = NONE. Encodage : algorithme Google/OSRM, precision 5 (facteur 1e5), deltas dans l'ordre (latitude, longitude). Decodable par @mapbox/polyline. Sur un trajet multi-points, couvre tout le parcours.","nullable":true,"example":"ydlrHnwfA~A_@dGsT"}},"description":"Itineraire calcule entre deux points."},"MatrixRequest":{"required":["points"],"type":"object","properties":{"points":{"type":"array","description":"Liste des points (au moins 1). L'ordre est conserve dans la matrice resultat.","items":{"$ref":"#/components/schemas/Coordinate"}}},"description":"Demande de matrice de temps entre N points."},"MatrixResponse":{"type":"object","properties":{"size":{"type":"integer","description":"Nombre de points (N).","format":"int32","example":3},"durationsSeconds":{"type":"array","description":"Temps de trajet en secondes : durationsSeconds[i][j] = i -> j. Diagonale = 0.","example":[[0,4380,9200],[4350,0,8100],[9150,8050,0]],"items":{"type":"array","description":"Temps de trajet en secondes : durationsSeconds[i][j] = i -> j. Diagonale = 0.","example":[[0,4380,9200],[4350,0,8100],[9150,8050,0]],"items":{"type":"integer","description":"Temps de trajet en secondes : durationsSeconds[i][j] = i -> j. Diagonale = 0.","format":"int64"}}}},"description":"Matrice NxN des temps de trajet."},"OptimizeRequest":{"required":["depot"],"type":"object","properties":{"depot":{"$ref":"#/components/schemas/Coordinate"},"vehicleCount":{"maximum":1000,"type":"integer","description":"Nombre de vehicules, au plus 1000. 1 = optimisation d'ordre simple (TSP). Defaut : 1.","format":"int32","example":1,"default":1},"vehicleCapacity":{"type":"integer","description":"Capacite par vehicule (memes unites que `demand`). Null = illimite.","format":"int32","nullable":true,"example":10},"departureTime":{"type":"string","description":"Heure de depart ESTIMEE du depot (ISO-8601 LocalDateTime, heure locale, sans fuseau). C'est l'origine des temps de toute la tournee : les heures d'arrivee/depart de chaque arret en decoulent ET c'est par rapport a elle que le solveur evalue les fenetres horaires (`timeWindowStart`/`timeWindowEnd` des visites). A renseigner des qu'au moins une visite a une fenetre horaire, sinon l'heure courante du serveur est utilisee (ce qui peut rendre des fenetres infaisables si la requete est calculee bien avant le depart reel).","format":"date-time","nullable":true},"maxWaitingSeconds":{"minimum":0,"type":"integer","description":"Attente MAXIMALE toleree devant une fenetre horaire pas encore ouverte, en secondes. Si le vehicule arriverait en avance de plus que cette valeur sur `timeWindowStart`, l'arret est considere comme NE CORRESPONDANT PAS au creneau (contrainte dure) : le solveur reordonne la tournee pour l'eviter et, si aucun ordre ne le permet, la tournee est renvoyee avec `feasible=false`, `stops[].timeWindowStatus=WAITING_TOO_LONG` et `stops[].excessiveWaitingSeconds` > 0. Evite les tournees ou le vehicule poireaute 3 h devant un client. Omis (null) = valeur serveur `app.optimization.max-waiting-seconds` (3600 s = 1 h par defaut). `0` = DESACTIVE (attente illimitee, seulement minimisee en soft). La valeur effectivement appliquee est renvoyee dans `maxWaitingSeconds` de la reponse.","format":"int32","nullable":true,"example":3600,"default":3600},"includeGeometry":{"type":"boolean","description":"[Deprecie : preferer geometryFormat] Inclure la geometrie de chaque segment. Ignore si geometryFormat est fourni. true -> POINTS, false -> NONE.","nullable":true,"example":true},"geometryFormat":{"type":"string","description":"Format de la geometrie :\n- POINTS   : liste de points [lat,lon] (champ `geometry`). Lisible mais volumineux.\n- POLYLINE : chaine encodee (champ `geometryPolyline`), algorithme Google/OSRM precision 5 (~1 m), bien plus compacte pour les longs trajets. Decodable avec @mapbox/polyline, etc.\n- NONE     : pas de geometrie (reponse la plus legere).","nullable":true,"default":"POINTS","enum":["POINTS","POLYLINE","NONE"]},"maxSolvingSeconds":{"minimum":1,"type":"integer","description":"Temps de RESOLUTION alloue au solveur, en secondes. FACULTATIF et sans effet sur une tournee classique : omis, le serveur applique sa terminaison globale (`timefold.solver.termination.spent-limit`, 1 s), ce qui suffit pour ordonner des points independants. En revanche, DES QUE la requete contient des `shipments`, le probleme est plus difficile (l'etat initial ignore la precedence, le solveur doit d'abord la reparer) et un budget plus long est applique : valeur de ce champ, sinon defaut serveur `app.optimization.shipments.default-solving-seconds` (5 s), plafonne SILENCIEUSEMENT a `app.optimization.shipments.max-solving-seconds` (60 s). La requete HTTP dure alors au moins ce temps. La valeur reellement appliquee est renvoyee dans `solvingTimeSeconds`.","format":"int32","nullable":true,"example":5},"visits":{"type":"array","description":"Points a visiter, independants les uns des autres : le solveur est libre de les ordonner comme il veut. Chaque point peut porter une duree de service et une fenetre horaire optionnelles. Peut etre vide (ou absent) si `shipments` est fourni, mais la requete doit contenir AU MOINS un point ou une mission.","nullable":true,"items":{"$ref":"#/components/schemas/VisitDto"}},"shipments":{"type":"array","description":"MISSIONS APPAIREES (chargement -> enlevement), facultatives. A utiliser quand une marchandise doit etre chargee a un endroit puis deposee a un autre : l'API garantit alors que les deux arrets sont sur le MEME vehicule et que le chargement passe AVANT l'enlevement. Peuvent etre melangees librement avec `visits`. Omises = comportement historique, a l'identique.","nullable":true,"items":{"$ref":"#/components/schemas/ShipmentDto"}}},"description":"Demande d'optimisation de tournee : depot commun, vehicules, heure de depart estimee, et points a visiter, fournis sous deux formes combinables — `visits` (arrets independants, ordonnes librement) et `shipments` (missions appairees chargement -> enlevement, servies par le meme vehicule, chargement d'abord). Au moins un point ou une mission est requis."},"ShipmentDto":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant de la mission. Repris tel quel dans `stops[].shipmentId` de la reponse, ce qui permet de recoller les deux arrets. Auto-genere (`s<index>`) si absent. Doit etre unique dans la requete.","nullable":true,"example":"M1"},"name":{"type":"string","description":"Libelle de la mission, pour l'affichage. Sans effet sur l'optimisation.","nullable":true,"example":"Palette Dupont"},"pickup":{"$ref":"#/components/schemas/VisitDto"},"delivery":{"$ref":"#/components/schemas/VisitDto"}},"description":"MISSION APPAIREE : une marchandise chargee a un endroit (`pickup`) puis enlevee/deposee a un autre (`delivery`). L'API garantit alors deux regles que `visits` seul ne permet pas d'exprimer :\n1. les DEUX arrets sont servis par le MEME vehicule ;\n2. le CHARGEMENT passe AVANT l'enlevement dans l'ordre de la tournee.\n\nUne mission peut n'avoir qu'UNE extremite : l'autre bout est alors le depot (`pickup` absent = marchandise chargee au depot au depart, cas de la livraison classique ; `delivery` absent = marchandise ramenee au depot en fin de tournee, cas de la collecte). Comme le depot encadre deja la tournee, aucune contrainte de precedence n'est necessaire dans ce cas : l'arret se comporte comme un point de `visits`.\n\nLes deux extremites sont des `VisitDto` ordinaires : coordonnees, duree de service et fenetre horaire propres a CHAQUE extremite (on peut donc exiger un chargement le matin et une depose l'apres-midi). ATTENTION au champ `demand` : il garde sa semantique habituelle (somme comparee a `vehicleCapacity` sur l'ensemble de la tournee, PAS un suivi de la charge a bord). Le porter sur le `pickup` et laisser `delivery.demand` a 0, sinon la charge est comptee deux fois.","nullable":true},"VisitDto":{"required":["lat","lon"],"type":"object","properties":{"id":{"type":"string","description":"Identifiant du point (optionnel ; auto-genere si absent). S'il est fourni, il doit etre unique dans la requete, sinon 400.","example":"A"},"name":{"type":"string","description":"Libelle lisible (optionnel).","example":"Pharmacie du Centre"},"lat":{"maximum":90,"exclusiveMaximum":false,"minimum":-90,"exclusiveMinimum":false,"type":"number","description":"Latitude WGS84, dans [-90, 90].","format":"double","example":47.2184},"lon":{"maximum":180,"exclusiveMaximum":false,"minimum":-180,"exclusiveMinimum":false,"type":"number","description":"Longitude WGS84, dans [-180, 180].","format":"double","example":-1.5536},"demand":{"type":"integer","description":"Demande / charge consommee a ce point (optionnel ; defaut 0). Comparee a la capacite du vehicule.","format":"int32","example":1},"serviceDurationSeconds":{"type":"integer","description":"Duree d'arret / de service a ce point en secondes (optionnel ; defaut 0). Decale les heures d'arrivee des points suivants.","format":"int32","example":300},"timeWindowStart":{"type":"string","description":"Debut de la fenetre horaire de livraison (ISO-8601 LocalDateTime, MEME jour/reference que `departureTime` de la requete). Si le vehicule arrive AVANT cette heure, il ATTEND sur place (attente comptee dans `waitingSeconds` et minimisee par le solveur). Null = pas de borne basse (on peut livrer des le depart).","format":"date-time","nullable":true},"timeWindowEnd":{"type":"string","description":"Fin de la fenetre horaire : heure LIMITE d'ARRIVEE au point (ISO-8601 LocalDateTime). Contrainte DURE du solveur : il fait tout pour arriver avant. Si c'est impossible (fenetre deja passee a `departureTime`, trop de points...), l'arret est quand meme planifie mais `lateSeconds` > 0, `feasible` = false et le score `hard` est negatif. Null = pas de borne haute (« pas d'importance »).","format":"date-time","nullable":true}},"description":"Point a visiter dans une tournee. La fenetre horaire (`timeWindowStart`/`timeWindowEnd`) est OPTIONNELLE : les deux absents = aucune contrainte horaire (« pas d'importance »). On peut ne donner qu'une borne (ex : seulement `timeWindowEnd` = « avant 23h », seulement `timeWindowStart` = « pas avant 21h »).","nullable":true},"LegDto":{"type":"object","properties":{"distanceMeters":{"type":"number","description":"Distance du segment, en metres.","format":"double","example":52300.0},"durationSeconds":{"type":"integer","description":"Duree de conduite du segment, en secondes.","format":"int64","example":4380},"geometry":{"type":"array","description":"Trace en points [lat,lon]. Non nul uniquement si geometryFormat = POINTS.","nullable":true,"example":[[48.1173,-1.6778],[48.0,-1.7],[47.2184,-1.5536]],"items":{"type":"array","description":"Trace en points [lat,lon]. Non nul uniquement si geometryFormat = POINTS.","nullable":true,"example":[[48.1173,-1.6778],[48.0,-1.7],[47.2184,-1.5536]],"items":{"type":"number","description":"Trace en points [lat,lon]. Non nul uniquement si geometryFormat = POINTS.","format":"double","nullable":true}}},"geometryPolyline":{"type":"string","description":"Trace en polyligne encodee (precision 5). Non nul uniquement si geometryFormat = POLYLINE.","nullable":true,"example":"ydlrHnwfA~A_@dGsT"}},"description":"Un segment routier : distance, duree et geometrie (selon geometryFormat)."},"OptimizeResponse":{"type":"object","properties":{"score":{"type":"string","description":"Score Timefold (`<dur>hard/<soft>soft`). `0hard` = contraintes dures respectees (capacite, fenetres horaires ET missions appairees). Le `soft` = temps de conduite + temps d'attente (s), minimise.","example":"0hard/-12450soft"},"feasible":{"type":"boolean","description":"true si TOUTES les contraintes dures sont respectees : aucune capacite depassee, aucun arret en retard sur sa fenetre horaire (`lateSeconds` = 0 partout), aucune attente au-dela de `maxWaitingSeconds` (`excessiveWaitingSeconds` = 0 partout) et aucune mission appairee cassee (`pairingViolations` = 0). false = la tournee est quand meme renvoyee (meilleure solution trouvee) mais au moins une contrainte est violee : inspecter `timeWindowViolations`, `pairingViolations` et les `stops[].timeWindowStatus`.","example":true},"timeWindowViolations":{"type":"integer","description":"Nombre d'arrets dont la fenetre horaire n'est PAS respectee : arrivee apres `timeWindowEnd` (`timeWindowStatus=LATE`) OU attente avant `timeWindowStart` superieure a `maxWaitingSeconds` (`timeWindowStatus=WAITING_TOO_LONG`). 0 si toutes les fenetres sont tenues ou si aucune fenetre n'a ete fournie.","format":"int32","example":0},"pairingViolations":{"type":"integer","description":"Nombre de MISSIONS appairees (`shipments`) dont la regle n'est pas tenue dans les tournees renvoyees : les deux arrets sur des vehicules differents, ou l'enlevement place avant son chargement. Recompte sur la reponse elle-meme, independamment du score. 0 = toutes les missions sont correctement enchainees (et toujours 0 si la requete n'en contenait aucune).","format":"int32","example":0},"maxWaitingSeconds":{"type":"integer","description":"Attente maximale toleree devant une fenetre horaire effectivement appliquee, en secondes (valeur de la requete ou defaut serveur). null = pas de limite (desactive par `0`).","format":"int32","nullable":true,"example":3600},"solvingTimeSeconds":{"type":"integer","description":"Temps de resolution reellement alloue au solveur, en secondes. null = terminaison globale du serveur (`timefold.solver.termination.spent-limit`, 1 s), cas d'une tournee sans mission appairee et sans `maxSolvingSeconds`. Renseigne des que la requete contient des `shipments` ou un `maxSolvingSeconds` (valeur demandee, plafonnee par la config serveur).","format":"int32","nullable":true,"example":5},"totalDrivingTimeSeconds":{"type":"integer","description":"Temps de conduite total cumule sur tous les vehicules, en secondes.","format":"int64","example":12450},"totalDistanceMeters":{"type":"number","description":"Distance totale parcourue par tous les vehicules, en metres.","format":"double","example":184300.0},"routes":{"type":"array","description":"Une entree par vehicule.","items":{"$ref":"#/components/schemas/RouteDto"}},"skippedVisits":{"type":"array","description":"Visites EXCLUES de l'optimisation car non rattachables au reseau routier (coordonnees en mer, hors de la zone OSM couverte, ou trop eloignees de toute route selon `app.routing.max-snap-distance-meters`), ou parce que l'autre extremite de leur mission l'etait (`PAIRED_POINT_SKIPPED` : une mission appairee est ecartee en entier ou pas du tout, sinon la tournee serait structurellement infaisable). Ces points ne figurent dans AUCUNE tournee : ils sont ignores pour ne pas faire echouer toute la requete. Liste vide si tous les points ont ete pris en compte. Le client DOIT verifier ce tableau et, le cas echeant, signaler/corriger ces points.","items":{"$ref":"#/components/schemas/SkippedVisitDto"}}},"description":"Resultat d'optimisation : tournees ordonnees par vehicule, avec distances, heures d'arrivee/depart, respect des fenetres horaires et geometrie du trace."},"RouteDto":{"type":"object","properties":{"vehicleId":{"type":"string","description":"Identifiant du vehicule.","example":"vehicle-0"},"departureTime":{"type":"string","description":"Heure de depart du depot (= `departureTime` de la requete).","format":"date-time"},"returnTime":{"type":"string","description":"Heure de retour au depot (fin de tournee), attentes incluses.","format":"date-time"},"drivingTimeSeconds":{"type":"integer","description":"Temps de conduite total de la tournee, en secondes.","format":"int64","example":12450},"serviceTimeSeconds":{"type":"integer","description":"Temps de service total (arrets) de la tournee, en secondes.","format":"int64","example":1500},"waitingTimeSeconds":{"type":"integer","description":"Temps d'attente total de la tournee, en secondes : somme des `waitingSeconds` des arrets (vehicule arrive avant l'ouverture d'une fenetre horaire). 0 sans fenetres.","format":"int64","example":600},"durationSeconds":{"type":"integer","description":"Duree TOTALE de la tournee, en secondes, du depart du depot au retour : `drivingTimeSeconds` + `serviceTimeSeconds` + `waitingTimeSeconds` (= `returnTime` - `departureTime`). C'est la duree comparee entre vehicules par `/optimization/dispatch`. 0 pour un vehicule sans arret.","format":"int64","example":14550},"distanceMeters":{"type":"number","description":"Distance totale de la tournee, en metres.","format":"double","example":184300.0},"totalDemand":{"type":"integer","description":"Somme des demandes des visites de la tournee.","format":"int32","example":2},"stops":{"type":"array","description":"Arrets dans l'ordre optimal de passage.","items":{"$ref":"#/components/schemas/StopDto"}},"returnLeg":{"$ref":"#/components/schemas/LegDto"},"geometry":{"type":"array","description":"Trace COMPLETE de la tournee (depot -> arrets dans l'ordre -> depot) en liste de points [lat,lon], continue et sans doublon aux jonctions. Non nul uniquement si geometryFormat = POINTS. A privilegier pour tracer toute la tournee d'un seul trait : les polylignes par segment ne sont pas concatenables.","nullable":true,"example":[[48.1173,-1.6778],[48.05,-1.7],[47.2184,-1.5536]],"items":{"type":"array","description":"Trace COMPLETE de la tournee (depot -> arrets dans l'ordre -> depot) en liste de points [lat,lon], continue et sans doublon aux jonctions. Non nul uniquement si geometryFormat = POINTS. A privilegier pour tracer toute la tournee d'un seul trait : les polylignes par segment ne sont pas concatenables.","nullable":true,"example":[[48.1173,-1.6778],[48.05,-1.7],[47.2184,-1.5536]],"items":{"type":"number","description":"Trace COMPLETE de la tournee (depot -> arrets dans l'ordre -> depot) en liste de points [lat,lon], continue et sans doublon aux jonctions. Non nul uniquement si geometryFormat = POINTS. A privilegier pour tracer toute la tournee d'un seul trait : les polylignes par segment ne sont pas concatenables.","format":"double","nullable":true}}},"geometryPolyline":{"type":"string","description":"Trace COMPLETE de la tournee (depot -> arrets -> depot) en polyligne encodee (Google/OSRM, precision 5, ordre lat/lon). Renseignee (non nul) pour POINTS et POLYLINE ; null uniquement si geometryFormat = NONE. Champ a utiliser pour afficher le trace global de la tournee (les `legFromPrevious.geometryPolyline` ne se concatenent pas entre eux).","nullable":true,"example":"ydlrHnwfA~A_@dGsT"}},"description":"Tournee d'un vehicule : visites dans l'ordre, depot -> points -> depot."},"SkippedVisitDto":{"type":"object","properties":{"visitId":{"type":"string","description":"Identifiant de la visite (celui fourni, ou auto-genere `v<index>`).","example":"A"},"name":{"type":"string","description":"Libelle de la visite (tel que fourni).","nullable":true,"example":"Client A"},"lat":{"type":"number","description":"Latitude fournie.","format":"double","example":47.2184},"lon":{"type":"number","description":"Longitude fournie.","format":"double","example":-1.5536},"reason":{"type":"string","description":"Motif d'exclusion : `UNROUTABLE` (aucune route trouvable a proximite : point en mer, hors zone couverte, ou reseau deconnecte), `TOO_FAR` (route la plus proche au-dela du seuil `app.routing.max-snap-distance-meters`) ou `PAIRED_POINT_SKIPPED` (ce point est rattachable, mais l'AUTRE extremite de sa mission ne l'est pas : une mission ne peut pas etre executee a moitie, les deux arrets sont donc ecartes ensemble).","example":"TOO_FAR","enum":["UNROUTABLE","TOO_FAR","PAIRED_POINT_SKIPPED"]},"snapDistanceMeters":{"type":"number","description":"Distance (m) jusqu'a la route la plus proche. Renseignee pour `TOO_FAR` ; null pour `UNROUTABLE` (aucune route trouvee) et `PAIRED_POINT_SKIPPED`.","format":"double","nullable":true,"example":1830.0},"shipmentId":{"type":"string","description":"Identifiant de la mission dont ce point est une extremite. null pour un arret simple (issu de `visits`).","nullable":true,"example":"M1"},"stopType":{"type":"string","description":"Role du point dans sa mission : `PICKUP` (chargement) ou `DELIVERY` (enlevement). null pour un arret simple.","nullable":true,"example":"PICKUP","enum":["PICKUP","DELIVERY"]}},"description":"Visite ecartee de l'optimisation, avec le motif. N'apparait dans aucune tournee."},"StopDto":{"type":"object","properties":{"visitId":{"type":"string","description":"Identifiant du point visite.","example":"A"},"name":{"type":"string","description":"Libelle du point.","example":"Pharmacie du Centre"},"lat":{"type":"number","description":"Latitude.","format":"double","example":47.2184},"lon":{"type":"number","description":"Longitude.","format":"double","example":-1.5536},"legFromPrevious":{"$ref":"#/components/schemas/LegDto"},"cumulativeDistanceMeters":{"type":"number","description":"Distance cumulee depuis le depot jusqu'a ce point, en metres.","format":"double","example":52300.0},"cumulativeDrivingSeconds":{"type":"integer","description":"Temps de conduite cumule depuis le depot jusqu'a ce point, en secondes.","format":"int64","example":4380},"arrivalTime":{"type":"string","description":"Heure d'ARRIVEE physique au point (depart du point precedent + trajet).","format":"date-time"},"serviceStartTime":{"type":"string","description":"Heure de DEBUT du service/livraison : = `arrivalTime`, ou = `timeWindowStart` si le vehicule est arrive en avance et a attendu l'ouverture de la fenetre.","format":"date-time"},"departureTime":{"type":"string","description":"Heure de depart de ce point (`serviceStartTime` + duree de service).","format":"date-time"},"timeWindowStart":{"type":"string","description":"Fenetre horaire demandee (debut), telle que fournie. null si non fournie.","format":"date-time","nullable":true},"timeWindowEnd":{"type":"string","description":"Fenetre horaire demandee (fin = heure limite d'arrivee), telle que fournie. null si non fournie.","format":"date-time","nullable":true},"waitingSeconds":{"type":"integer","description":"Attente sur place avant l'ouverture de la fenetre, en secondes (`serviceStartTime` - `arrivalTime`). 0 si pas d'attente / pas de fenetre. Si elle depasse `maxWaitingSeconds`, l'arret est en VIOLATION (`timeWindowStatus=WAITING_TOO_LONG`) ; les heures restent calculees comme si le vehicule attendait l'ouverture.","format":"int64","example":480},"excessiveWaitingSeconds":{"type":"integer","description":"Part de l'attente qui depasse `maxWaitingSeconds` (`waitingSeconds` - limite si positif). 0 = attente toleree (ou pas de limite). > 0 = VIOLATION : le solveur n'a pas trouve d'ordre evitant d'arriver aussi tot ; l'arret ne correspond pas au creneau demande (voir `feasible`).","format":"int64","example":0},"lateSeconds":{"type":"integer","description":"Retard par rapport a `timeWindowEnd`, en secondes (`arrivalTime` - `timeWindowEnd` si positif). 0 = fenetre respectee (ou pas de fenetre). > 0 = VIOLATION : le solveur n'a pas trouve d'ordre permettant d'arriver a temps (voir `feasible`).","format":"int64","example":0},"timeWindowStatus":{"type":"string","description":"Respect de la fenetre horaire de cet arret : `OK` (fenetre tenue, ou pas de fenetre), `LATE` (arrivee apres `timeWindowEnd`, voir `lateSeconds`), `WAITING_TOO_LONG` (arrivee trop en avance : attente > `maxWaitingSeconds`, voir `excessiveWaitingSeconds`). Tout statut autre que `OK` compte dans `timeWindowViolations` et rend `feasible=false`.","example":"OK","enum":["OK","LATE","WAITING_TOO_LONG"]},"demand":{"type":"integer","description":"Demande consommee a ce point.","format":"int32","example":1},"shipmentId":{"type":"string","description":"Identifiant de la mission (`shipments[].id`) dont cet arret est une extremite. null pour un arret simple issu de `visits`. Les deux arrets d'une meme mission portent le meme `shipmentId` : c'est ce qui permet de les recoller cote client.","nullable":true,"example":"M1"},"stopType":{"type":"string","description":"Role de l'arret dans sa mission : `PICKUP` (chargement de la marchandise) ou `DELIVERY` (enlevement / depose). null pour un arret simple. Dans une tournee valide, le `PICKUP` d'une mission apparait TOUJOURS avant son `DELIVERY`, dans la meme tournee.","nullable":true,"example":"PICKUP","enum":["PICKUP","DELIVERY"]}},"description":"Un arret de la tournee, avec le segment depuis le point precedent, les cumuls, les heures (arrivee, debut de service, depart) et le respect de la fenetre horaire."},"DispatchRequest":{"required":["depot","vehicleCount"],"type":"object","properties":{"depot":{"$ref":"#/components/schemas/Coordinate"},"vehicleCount":{"maximum":1000,"minimum":1,"type":"integer","description":"Nombre de tournees (= de vehicules) a produire. REQUIS, entre 1 et 1000. Le solveur utilise tous les vehicules des que les points le permettent (avec plus de vehicules que de points, les vehicules excedentaires ont une tournee vide). 1 = simple optimisation d'ordre (TSP).","format":"int32","example":5},"vehicleCapacity":{"type":"integer","description":"Capacite par vehicule (memes unites que `demand` des visites). FACULTATIF : la repartition n'en a pas besoin (elle se fait sur les durees). Si fournie, elle s'ajoute comme contrainte DURE (une tournee ne peut pas depasser cette charge). Null = illimitee.","format":"int32","nullable":true,"example":40},"departureTime":{"type":"string","description":"Heure de depart ESTIMEE du depot, commune a toutes les tournees (ISO-8601 LocalDateTime, heure locale, sans fuseau). Origine des temps : toutes les heures de la reponse en decoulent et les fenetres horaires (`timeWindowStart`/`timeWindowEnd` des visites) sont evaluees par rapport a elle. Omise = heure courante du serveur. A TOUJOURS renseigner des qu'au moins une visite a une fenetre horaire.","format":"date-time","nullable":true},"maxWaitingSeconds":{"minimum":0,"type":"integer","description":"Attente MAXIMALE toleree devant une fenetre horaire pas encore ouverte, en secondes (meme semantique que sur `/optimize`). Au-dela, l'arret est considere comme ne correspondant pas au creneau (contrainte dure -> `feasible=false`, `timeWindowStatus=WAITING_TOO_LONG` si aucune repartition ne l'evite). Omis = defaut serveur `app.optimization.max-waiting-seconds` (3600 s). `0` = desactive (attente illimitee, seulement minimisee).","format":"int32","nullable":true,"example":3600,"default":3600},"maxSolvingSeconds":{"minimum":1,"type":"integer","description":"Temps de RESOLUTION alloue au solveur, en secondes. La requete HTTP dure au moins ce temps (plus le calcul de la matrice : ~101x101 routages pour 100 points, quelques secondes). Plus il est long, meilleure est la repartition ; pour ~100 points / 5 tournees, 10 s donne un resultat correct et 20-30 s un tres bon resultat. Omis = defaut serveur `app.optimization.dispatch.default-solving-seconds` (10 s). Plafonne SILENCIEUSEMENT a `app.optimization.dispatch.max-solving-seconds` (60 s) : la valeur effectivement appliquee est renvoyee dans `solvingTimeSeconds`.","format":"int32","nullable":true,"example":20,"default":10},"geometryFormat":{"type":"string","description":"Format de la geometrie :\n- POINTS   : liste de points [lat,lon] (champ `geometry`). Lisible mais volumineux.\n- POLYLINE : chaine encodee (champ `geometryPolyline`), algorithme Google/OSRM precision 5 (~1 m), bien plus compacte pour les longs trajets. Decodable avec @mapbox/polyline, etc.\n- NONE     : pas de geometrie (reponse la plus legere).","nullable":true,"default":"POINTS","enum":["POINTS","POLYLINE","NONE"]},"visits":{"type":"array","description":"Points a repartir puis visiter (typiquement 20 a 300), independants les uns des autres. Chaque point peut porter une duree de service et une fenetre horaire optionnelles. `demand` n'est utile que si `vehicleCapacity` est fourni. Peut etre vide (ou absent) si `shipments` est fourni, mais la requete doit contenir AU MOINS un point ou une mission.","nullable":true,"items":{"$ref":"#/components/schemas/VisitDto"}},"shipments":{"type":"array","description":"MISSIONS APPAIREES (chargement -> enlevement), facultatives. Une mission n'est JAMAIS coupee entre deux tournees : ses deux arrets partent dans la meme, le chargement d'abord. La repartition equilibree se fait donc sur des missions entieres. Peuvent etre melangees librement avec `visits`. Omises = comportement historique, a l'identique.","nullable":true,"items":{"$ref":"#/components/schemas/ShipmentDto"}}},"description":"Demande de repartition : un depot commun, un NOMBRE de tournees voulu et une liste de points (typiquement plusieurs dizaines a plusieurs centaines). L'API repartit elle-meme les points entre les tournees en EQUILIBRANT LES DUREES (conduite + service + attentes) et en formant des zones geographiques coherentes, puis ordonne chaque tournee. Aucune capacite n'est necessaire. Les fenetres horaires optionnelles par point sont respectees comme sur `/optimize`. Les points sont fournis sous deux formes combinables : `visits` (arrets independants) et `shipments` (missions appairees chargement -> enlevement, jamais coupees entre deux tournees)."},"BalanceDto":{"type":"object","properties":{"longestRouteSeconds":{"type":"integer","description":"Duree de la tournee la plus longue.","format":"int64","example":8340},"shortestRouteSeconds":{"type":"integer","description":"Duree de la tournee la plus courte (parmi celles qui ont au moins un arret).","format":"int64","example":7410},"averageRouteSeconds":{"type":"integer","description":"Duree moyenne des tournees utilisees (arrondie).","format":"int64","example":7866},"spreadSeconds":{"type":"integer","description":"Ecart entre la plus longue et la plus courte (`longest - shortest`). Plus il est petit, plus la charge est equitable. Un ecart notable est normal quand les fenetres horaires ou la geographie l'imposent : l'objectif reste le temps global, pas l'egalite stricte.","format":"int64","example":930}},"description":"Equilibre des durees entre les tournees utilisees. Toutes les valeurs sont en secondes et portent sur `routes[].durationSeconds` (conduite + service + attente). Zeros si aucune tournee n'a d'arret."},"DispatchResponse":{"type":"object","properties":{"score":{"type":"string","description":"Score Timefold (`<dur>hard/<soft>soft`). `0hard` = toutes les contraintes dures sont respectees (fenetres horaires, attente max, capacite si fournie). Le `soft` est domine par la somme des CARRES des durees de tournee (s^2) + conduite + attente : il n'a de sens que pour comparer deux resolutions du MEME probleme, pas entre requetes.","example":"0hard/-84567321soft"},"feasible":{"type":"boolean","description":"true si TOUTES les contraintes dures sont respectees : aucun arret en retard sur sa fenetre, aucune attente au-dela de `maxWaitingSeconds`, aucune capacite depassee et aucune mission appairee cassee. false = les tournees sont quand meme renvoyees (meilleure solution trouvee) : inspecter `timeWindowViolations`, `pairingViolations` et `stops[].timeWindowStatus`.","example":true},"timeWindowViolations":{"type":"integer","description":"Nombre d'arrets dont la fenetre horaire n'est PAS respectee (`LATE` ou `WAITING_TOO_LONG`). 0 si toutes les fenetres sont tenues ou si aucune n'a ete fournie.","format":"int32","example":0},"pairingViolations":{"type":"integer","description":"Nombre de MISSIONS appairees (`shipments`) dont la regle n'est pas tenue : les deux arrets repartis dans des tournees differentes, ou l'enlevement place avant son chargement. 0 = toutes les missions sont intactes (et toujours 0 sans `shipments` dans la requete).","format":"int32","example":0},"maxWaitingSeconds":{"type":"integer","description":"Attente maximale toleree devant une fenetre horaire effectivement appliquee, en secondes. null = pas de limite (desactive par `0`).","format":"int32","nullable":true,"example":3600},"solvingTimeSeconds":{"type":"integer","description":"Temps de resolution effectivement alloue au solveur, en secondes (valeur de la requete plafonnee par le serveur, ou defaut serveur).","format":"int32","example":20},"vehicleCount":{"type":"integer","description":"Nombre de tournees demande (`vehicleCount` de la requete) = taille de `routes`.","format":"int32","example":5},"usedVehicleCount":{"type":"integer","description":"Nombre de tournees contenant au moins un arret. Inferieur a `vehicleCount` seulement s'il y a moins de points valides que de vehicules.","format":"int32","example":5},"totalDrivingTimeSeconds":{"type":"integer","description":"Temps de conduite total cumule sur toutes les tournees, en secondes.","format":"int64","example":21330},"totalDistanceMeters":{"type":"number","description":"Distance totale parcourue par toutes les tournees, en metres.","format":"double","example":412800.0},"totalDurationSeconds":{"type":"integer","description":"Somme des durees de tournee (`routes[].durationSeconds` : conduite + service + attente), en secondes.","format":"int64","example":39330},"balance":{"$ref":"#/components/schemas/BalanceDto"},"routes":{"type":"array","description":"Une entree par vehicule, dans l'ordre `vehicle-0` .. `vehicle-<K-1>`. Chaque tournee est deja ordonnee (ordre optimal de passage) et couvre une zone geographique coherente. Une tournee peut etre vide (aucun arret) s'il y a plus de vehicules que de points.","items":{"$ref":"#/components/schemas/RouteDto"}},"skippedVisits":{"type":"array","description":"Visites EXCLUES car non rattachables au reseau routier (`UNROUTABLE`), trop eloignees de toute route (`TOO_FAR`), ou parce que l'autre extremite de leur mission l'etait (`PAIRED_POINT_SKIPPED` : une mission est ecartee en entier ou pas du tout). Elles ne figurent dans AUCUNE tournee. Le client DOIT verifier ce tableau.","items":{"$ref":"#/components/schemas/SkippedVisitDto"}}},"description":"Resultat d'une repartition : K tournees equilibrees en duree (une par vehicule), chacune ordonnee et enrichie comme sur `/optimize` (heures, cumuls, geometrie), plus des indicateurs d'equilibre entre tournees."},"ComponentDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du sous-systeme.","example":"Routing"},"state":{"type":"string","description":"Etat.","example":"READY","enum":["WAITING","DOWNLOADING","INITIALIZING","READY","DISABLED","ERROR"]},"detail":{"type":"string","description":"Detail lisible de l'etat courant.","example":"Graphe routier pret (profil car)."},"ready":{"type":"boolean","description":"true si le sous-systeme repond aux requetes.","example":true}},"description":"Etat d'un sous-systeme."},"DataFileDto":{"type":"object","properties":{"name":{"type":"string","description":"Libelle.","example":"Reseau routier (OSM)"},"path":{"type":"string","description":"Chemin local.","example":"data/france-latest.osm.pbf"},"present":{"type":"boolean","description":"Present sur le disque.","example":true},"sizeBytes":{"type":"integer","description":"Taille en octets (-1 pour un dossier ou si absent).","format":"int64","example":5024490959}},"description":"Fichier ou dossier de donnees."},"DataUpdateDto":{"type":"object","properties":{"autoDownload":{"type":"boolean","description":"Telechargement/mise a jour automatiques actives (app.data.auto-download). false = aucune mise a jour, ni au demarrage ni par le cron.","example":true},"updateOnStart":{"type":"boolean","description":"Verification d'une nouvelle version au demarrage, une fois les moteurs charges (app.data.update-on-start).","example":true},"cron":{"type":"string","description":"Expression cron Spring (6 champs, fuseau Europe/Paris) de la mise a jour planifiee.","example":"0 0 8 * * SUN"},"nextScheduledAtMillis":{"type":"integer","description":"Prochaine execution planifiee (epoch millis) ; null si auto-download=false ou cron invalide.","format":"int64","nullable":true,"example":1757836800000},"last":{"$ref":"#/components/schemas/LastDataUpdateDto"}},"description":"Mise a jour automatique des donnees (OSM + BAN) : configuration et historique. Le pipeline est commun au demarrage et au cron : requete HEAD sur la source, comparaison de la date de publication (`Last-Modified`) avec la version locale, puis telechargement + reconstruction + bascule a chaud seulement si une version plus recente est publiee. Chaque execution est notifiee sur le webhook Discord."},"DownloadDto":{"type":"object","properties":{"name":{"type":"string","description":"Nom du fichier.","example":"france-latest.osm.pbf"},"downloadedBytes":{"type":"integer","description":"Octets recus.","format":"int64","example":1073741824},"totalBytes":{"type":"integer","description":"Taille totale annoncee (-1 si inconnue).","format":"int64","example":5078011689},"percent":{"type":"integer","description":"Pourcentage (-1 si taille inconnue).","format":"int32","example":21},"done":{"type":"boolean","description":"true une fois le telechargement termine (ou abandonne).","example":false}},"description":"Progression d'un telechargement de fichier de donnees."},"JvmDto":{"type":"object","properties":{"javaVersion":{"type":"string","example":"17.0.12"},"pid":{"type":"integer","format":"int64","example":12345},"cpuCores":{"type":"integer","format":"int32","example":8},"memUsedBytes":{"type":"integer","description":"Memoire heap utilisee (octets).","format":"int64","example":4294967296},"memMaxBytes":{"type":"integer","description":"Memoire heap maximale (octets).","format":"int64","example":17179869184},"memUsedPercent":{"type":"integer","description":"Pourcentage de heap utilise.","format":"int32","example":25}},"description":"Informations sur la JVM."},"LastDataUpdateDto":{"type":"object","properties":{"trigger":{"type":"string","description":"Qui a declenche la mise a jour.","example":"Demarrage de l'API","enum":["Demarrage de l'API","Planificateur hebdomadaire"]},"state":{"type":"string","description":"Etat : RUNNING (en cours), SUCCESS (au moins un jeu de donnees mis a jour ou installe, aucun echec), UP_TO_DATE (rien a faire : versions locales deja a jour), PARTIAL (un jeu de donnees en echec, l'autre OK), FAILED (tout en echec ou erreur inattendue).","example":"SUCCESS","enum":["RUNNING","SUCCESS","UP_TO_DATE","PARTIAL","FAILED"]},"startedAtMillis":{"type":"integer","description":"Debut de l'execution (epoch millis).","format":"int64","example":1757491260000},"finishedAtMillis":{"type":"integer","description":"Fin de l'execution (epoch millis) ; null tant que state=RUNNING.","format":"int64","nullable":true,"example":1757493780000},"summary":{"type":"string","description":"Resume lisible par jeu de donnees (etape en cours si RUNNING).","example":"OSM : mis a jour (version du 10/09/2026) - BAN : deja a jour (version du 10/09/2026)"}},"description":"Derniere execution de la mise a jour des donnees.","nullable":true},"LinksDto":{"type":"object","properties":{"swaggerUi":{"type":"string","example":"/swagger-ui.html"},"openApi":{"type":"string","example":"/v3/api-docs"}},"description":"Liens utiles."},"StatusResponse":{"type":"object","properties":{"application":{"type":"string","description":"Nom de l'application.","example":"spring-org"},"status":{"type":"string","description":"Etat global : UP (tout est pret), STARTING (chargement/telechargement en cours), DEGRADED (au moins un sous-systeme en erreur).","example":"UP","enum":["UP","STARTING","DEGRADED"]},"uptimeSeconds":{"type":"integer","description":"Temps ecoule depuis le demarrage, en secondes.","format":"int64","example":3600},"startedAtMillis":{"type":"integer","description":"Horodatage du demarrage (epoch millis).","format":"int64","example":1757491200000},"routingProfile":{"type":"string","description":"Profil de routage GraphHopper actif.","example":"car"},"addressCount":{"type":"integer","description":"Nombre d'adresses numerotees indexees (-1 si l'index n'est pas charge).","format":"int32","example":26123456},"poiCount":{"type":"integer","description":"Nombre de points d'interet indexes (0 si leur index n'est pas charge).","format":"int64","example":1250000},"jvm":{"$ref":"#/components/schemas/JvmDto"},"components":{"type":"array","description":"Etat de chaque sous-systeme (Routing, Geocoding, Points d'interet).","items":{"$ref":"#/components/schemas/ComponentDto"}},"downloads":{"type":"array","description":"Telechargements connus depuis le demarrage (en cours ou termines).","items":{"$ref":"#/components/schemas/DownloadDto"}},"data":{"type":"array","description":"Presence et taille des fichiers/dossiers de donnees.","items":{"$ref":"#/components/schemas/DataFileDto"}},"dataUpdate":{"$ref":"#/components/schemas/DataUpdateDto"},"links":{"$ref":"#/components/schemas/LinksDto"}},"description":"Etat complet de l'API (reponse de GET /status)."},"GeocodingStatus":{"type":"object","properties":{"ready":{"type":"boolean","description":"true si l'index des adresses est charge. Si false, `/search` renvoie 503.","example":true},"poiReady":{"type":"boolean","description":"true si l'index des points d'interet est charge. Si false, `/search` repond quand meme, mais sans resultats de type `poi` (index en construction apres le demarrage ou une mise a jour, fichier OSM absent, ou fonction desactivee).","example":true},"addressCount":{"type":"integer","description":"Nombre d'adresses numerotees indexees (-1 si l'index n'est pas charge).","format":"int64","example":26098608},"poiCount":{"type":"integer","description":"Nombre de points d'interet indexes.","format":"int64","example":1250000}},"description":"Etat des index de recherche."},"AddressResult":{"required":["city","houseNumber","label","lat","lon","postcode","score","street","type"],"type":"object","properties":{"label":{"type":"string","description":"Libelle complet et lisible, a afficher tel quel. Selon `type` : `housenumber` = `12 bis Rue du Bocage, 35000 Rennes` ; `street` = `Rue du Bocage, 35000 Rennes` ; `municipality` = `35000 Rennes` ; `poi` = `Carrefour, 12 Rue Dupont, 35000 Rennes` (nom, adresse si connue d'OpenStreetMap, commune).","example":"12 bis Rue du Bocage, 35000 Rennes"},"houseNumber":{"type":"string","description":"Numero de voie, avec son indice de repetition eventuel (`12 bis`). Vide si le resultat n'est pas une adresse precise (toujours pour `street` et `municipality` ; pour un `poi`, renseigne seulement si OpenStreetMap le connait).","example":"12 bis"},"street":{"type":"string","description":"Nom de la voie. Vide pour `municipality`, et pour un `poi` dont OpenStreetMap ne connait pas la rue.","example":"Rue du Bocage"},"postcode":{"type":"string","description":"Code postal. Pour une commune qui en a plusieurs : le plus frequent (tous sont cherchables). Pour un `poi` : celui de la voie la plus proche, ou vide si aucune a moins de 3 km et qu'OpenStreetMap ne le renseigne pas.","example":"35000"},"city":{"type":"string","description":"Commune. Pour un `poi` : celle de la voie la plus proche, meme regle que `postcode`.","example":"Rennes"},"lat":{"type":"number","description":"Latitude WGS84. `street` : une adresse reelle au milieu de la voie ; `municipality` : barycentre des adresses de la commune ; `poi` : son emplacement (centre du batiment s'il est dessine comme une surface).","format":"double","example":48.1147},"lon":{"type":"number","description":"Longitude WGS84 (memes regles que `lat`).","format":"double","example":-1.6794},"type":{"type":"string","description":"Nature du resultat : `housenumber` (adresse precise, renvoyee quand la requete contient un numero existant), `street` (voie, sans numero), `municipality` (commune), `poi` (point d'interet OpenStreetMap : commerce, service public, gare, site touristique...).","example":"street","enum":["housenumber","street","municipality","poi"]},"distanceMeters":{"type":"number","description":"Distance en metres entre la position fournie (`lat`/`lon` de la requete) et ce resultat. `null` si aucune position n'a ete fournie.","format":"double","nullable":true,"example":742.0},"score":{"type":"number","description":"Score de pertinence (plus eleve = plus pertinent). A n'utiliser que pour comparer les resultats d'UNE MEME requete : il combine correspondance des mots, couverture du nom, nature du resultat et proximite. Le tableau est deja trie par score decroissant.","format":"float","example":6.42},"name":{"type":"string","description":"Nom du point d'interet (`null` si `type` n'est pas `poi`). Si l'objet n'a pas de nom dans OpenStreetMap : son enseigne.","nullable":true,"example":"Carrefour Market"},"category":{"type":"string","description":"Categorie technique du point d'interet, au format de tag OpenStreetMap `cle=valeur` (`null` si `type` n'est pas `poi`). Stable : a utiliser pour choisir une icone.","nullable":true,"example":"shop=supermarket"},"categoryLabel":{"type":"string","description":"Categorie lisible en francais, sans accents (`null` si `type` n'est pas `poi`).","nullable":true,"example":"Supermarche"},"brand":{"type":"string","description":"Enseigne du point d'interet (tag OpenStreetMap `brand`), `null` si inconnue ou si `type` n'est pas `poi`.","nullable":true,"example":"Carrefour"}},"description":"Resultat de recherche : une adresse precise, une voie, une commune ou un point d'interet, avec ses coordonnees et son score de pertinence. Le champ `type` indique lequel ; les champs sans objet pour ce type sont vides (chaines) ou `null` (champs propres aux points d'interet)."}}}}