{
  "openapi": "3.1.0",
  "info": {
    "title": "API des points relais de Relais autour de moi",
    "version": "1.0.0",
    "summary": "Points relais, consignes automatiques et bureaux de poste en France autour d'un point ou dans une zone, suggestions de recherche et géocodage inverse.",
    "description": "API publique en lecture seule, sans clé, du site https://www.relaisautourdemoi.fr. Elle renvoie les points relais, consignes automatiques (lockers) et bureaux de poste de tous les transporteurs (Mondial Relay, La Poste et Colissimo, Chronopost et Pickup, DPD, Relais Colis, UPS, DHL, GLS, Amazon, Vinted Go, Colis Privé), avec leurs horaires de la semaine type et leurs exceptions connues.\n\nTrois routes : `/api/points` (points autour d'un point ou dans une zone rectangulaire), `/api/suggest` (suggestions de la barre de recherche : villes, points relais par leur nom, lieux repères, adresses) et `/api/reverse` (adresse lisible la plus proche d'une position).\n\nHoraires au format `HH:MM-HH:MM`, heure de Paris ; jours de la semaine de 0 (lundi) à 6 (dimanche). Les horaires et adresses viennent des sources ci-dessous et peuvent changer : à vérifier auprès du transporteur avant de se déplacer. Base mise à jour chaque semaine.\n\nDonnées : La Poste (data.laposte.fr, Licence Ouverte 2.0 d'Etalab), Mondial Relay / InPost (API publique de points InPost), OpenStreetMap (ODbL 1.0, attribution « © les contributeurs d'OpenStreetMap » obligatoire, partage à l'identique des bases dérivées), communes de geo.api.gouv.fr (Licence Ouverte). Adresses de /api/suggest et /api/reverse : Géoplateforme IGN, Base Adresse Nationale (Licence Ouverte).\n\nLimites de débit, par réseau du client (adresse IPv4, ou bloc IPv6 /64) et par minute : 300 pour `/api/points`, 240 pour `/api/suggest`, 30 pour `/api/reverse` ; au-delà, réponse 429. Le géocodeur de l'IGN est en outre plafonné pour tout le site : au-delà, `/api/suggest` répond sans adresses et `/api/reverse` sans libellé. Merci de rester raisonnable et de garder les réponses en cache.",
    "contact": {
      "name": "Relais autour de moi",
      "url": "https://www.relaisautourdemoi.fr/a-propos",
      "email": "contact@relaisautourdemoi.fr"
    },
    "license": {
      "name": "Licence Ouverte 2.0 (La Poste, communes, adresses IGN) et ODbL 1.0 (données OpenStreetMap)",
      "url": "https://www.etalab.gouv.fr/licence-ouverte-open-licence/"
    }
  },
  "externalDocs": {
    "description": "Sources des données et méthode",
    "url": "https://www.relaisautourdemoi.fr/a-propos"
  },
  "servers": [{ "url": "https://www.relaisautourdemoi.fr" }],
  "paths": {
    "/api/points": {
      "get": {
        "operationId": "listPoints",
        "summary": "Points relais autour d'un point ou dans une zone",
        "description": "Autour d'un point : `lat` et `lon` obligatoires, avec `radius` (rayon fixe) ou `near` (rayon élargi automatiquement). Dans une zone : `bbox` (prioritaire sur `lat` et `lon`). Les points sont triés du plus proche au plus éloigné autour d'un point ; dans une zone, au-delà de `limit`, un échantillon réparti sur toute la zone est renvoyé avec `total` et `truncated`.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "description": "Latitude du point de recherche (WGS 84, degrés décimaux). Obligatoire sans `bbox`.",
            "schema": { "type": "number", "minimum": -90, "maximum": 90 },
            "example": 45.96
          },
          {
            "name": "lon",
            "in": "query",
            "description": "Longitude du point de recherche (WGS 84, degrés décimaux). Obligatoire sans `bbox`.",
            "schema": { "type": "number", "minimum": -180, "maximum": 180 },
            "example": 5.36
          },
          {
            "name": "radius",
            "in": "query",
            "description": "Rayon de recherche en kilomètres autour de `lat`/`lon`, ramené entre 0,1 et 50. Ignoré si `near` est fourni.",
            "schema": { "type": "number", "minimum": 0.1, "maximum": 50, "default": 5 }
          },
          {
            "name": "near",
            "in": "query",
            "description": "Toute valeur non vide (par exemple `1`) active la recherche des plus proches : rayon de 2 km, doublé jusqu'à 40 km tant que moins de 10 points sont trouvés. Le rayon retenu est rendu dans `radiusKm`.",
            "schema": { "type": "string" },
            "example": "1"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre maximal de points. Autour d'un point : 1 à 500, 150 par défaut. Avec `bbox` : 1 à 3 000, 2 000 par défaut.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 3000 }
          },
          {
            "name": "c",
            "in": "query",
            "description": "Marques à garder, séparées par des virgules. Une marque sans réseau propre est étendue aux réseaux qui la servent (Colissimo : La Poste ; Chronopost et DPD : Pickup).",
            "schema": { "type": "string" },
            "example": "mondial-relay,chronopost"
          },
          {
            "name": "t",
            "in": "query",
            "description": "Types de points à garder, séparés par des virgules : `relay` (commerce relais), `locker` (consigne automatique), `post` (bureau ou agence de poste). Sans effet avec `bbox`.",
            "schema": { "type": "string" },
            "example": "locker"
          },
          {
            "name": "bbox",
            "in": "query",
            "description": "Zone rectangulaire `minLon,minLat,maxLon,maxLat` (degrés décimaux), 2 degrés de latitude sur 3 de longitude au plus.",
            "schema": { "type": "string", "pattern": "^-?[0-9.]+,-?[0-9.]+,-?[0-9.]+,-?[0-9.]+$" },
            "example": "2.30,48.84,2.40,48.88"
          }
        ],
        "responses": {
          "200": {
            "description": "Points trouvés. Autour d'un point : `points` (avec la distance `d`), `radiusKm` et `live` (connecteurs temps réel des transporteurs interrogés, souvent vide). Avec `bbox` : `points`, `total` et `truncated`.",
            "headers": {
              "Cache-Control": { "schema": { "type": "string" }, "description": "public, s-maxage=3600, stale-while-revalidate=86400 (sauf réponse complétée par un connecteur temps réel)" }
            },
            "content": {
              "application/json": {
                "schema": { "oneOf": [{ "$ref": "#/components/schemas/NearbyPoints" }, { "$ref": "#/components/schemas/AreaPoints" }] },
                "example": {
                  "points": [
                    {
                      "id": "lp-00001a",
                      "carrier": "la-poste",
                      "type": "post",
                      "name": "La Poste Ambérieu-en-Bugey",
                      "address": "38 Rue Alexandre Berard",
                      "postcode": "01500",
                      "city": "Ambérieu-en-Bugey",
                      "lat": 45.9598,
                      "lon": 5.3582,
                      "hours": [["09:00-12:00", "14:00-18:00"], ["09:00-12:00", "14:00-18:00"], ["09:00-12:00", "14:00-18:00"], ["09:00-12:00", "14:00-18:00"], ["09:00-12:00", "14:00-18:00"], ["09:00-12:00"], []],
                      "exceptions": [{ "date": "2026-11-11", "hours": [] }],
                      "services": ["pickup", "dropoff", "return"],
                      "info": "Bureau de Poste",
                      "source": "laposte",
                      "parking": [{ "kind": "lot", "d": 60 }],
                      "d": 0.141
                    }
                  ],
                  "radiusKm": 2,
                  "live": []
                }
              }
            }
          },
          "400": {
            "description": "Paramètres manquants ou invalides (lat et lon absents, bbox mal formée ou trop grande).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Paramètres lat et lon requis" }
              }
            }
          },
          "429": {
            "description": "Plus de 300 requêtes par minute depuis le même réseau.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Trop de requêtes, réessayez dans une minute." }
              }
            }
          }
        }
      }
    },
    "/api/suggest": {
      "get": {
        "operationId": "suggest",
        "summary": "Suggestions de recherche : villes, points relais, lieux, adresses",
        "description": "Suggestions pour un texte saisi (8 au plus) : villes de la base, points relais par leur nom et lieux repères (enseignes, gares, pharmacies, mairies…) autour de `lat`/`lon` (40 et 30 km au plus), puis adresses et rues du géocodeur de l'IGN. Sans position, seulement villes et adresses. La position est arrondie à 0,01 degré (environ 1 km).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Texte saisi, 2 à 120 caractères (au-delà, tronqué). Moins de 2 caractères : liste vide.",
            "schema": { "type": "string", "minLength": 2, "maxLength": 120 },
            "example": "carrefour city"
          },
          {
            "name": "lat",
            "in": "query",
            "description": "Latitude de référence (facultative, avec `lon`).",
            "schema": { "type": "number", "minimum": -90, "maximum": 90 },
            "example": 48.71
          },
          {
            "name": "lon",
            "in": "query",
            "description": "Longitude de référence (facultative, avec `lat`).",
            "schema": { "type": "number", "minimum": -180, "maximum": 180 },
            "example": 2.24
          },
          {
            "name": "local",
            "in": "query",
            "description": "Toute valeur non vide : sans le géocodeur de l'IGN (villes, points et lieux seulement, réponse immédiate).",
            "schema": { "type": "string" }
          },
          {
            "name": "lang",
            "in": "query",
            "description": "`en` : liens vers les pages anglaises (/en/…), distances et catégories en anglais.",
            "schema": { "type": "string", "enum": ["fr", "en"], "default": "fr" }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions, dans l'ordre d'affichage.",
            "headers": {
              "Cache-Control": { "schema": { "type": "string" }, "description": "public, s-maxage=86400, stale-while-revalidate=604800 (no-store si le géocodeur n'a pas répondu)" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SuggestionList" },
                "example": {
                  "items": [
                    {
                      "label": "Carrefour City",
                      "sub": "Mondial Relay · 4 Rue du Docteur Morere, Palaiseau · à 360 m",
                      "lat": 48.71151,
                      "lon": 2.24428,
                      "kind": "point",
                      "href": "/point/mondial-relay-carrefour-city-palaiseau--mr-fr000363"
                    },
                    {
                      "label": "Palaiseau",
                      "sub": "91120 · Essonne",
                      "lat": 48.7145,
                      "lon": 2.2458,
                      "kind": "city",
                      "href": "/points-relais/palaiseau"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Plus de 240 requêtes par minute depuis le même réseau : liste vide.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuggestionList" }, "example": { "items": [] } } }
          }
        }
      }
    },
    "/api/reverse": {
      "get": {
        "operationId": "reverseGeocode",
        "summary": "Adresse la plus proche d'une position",
        "description": "Géocodage inverse par la Géoplateforme de l'IGN (Base Adresse Nationale) : libellé de l'adresse la plus proche, ou `null` si aucune adresse n'est trouvée ou si le géocodeur ne répond pas.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude (WGS 84, degrés décimaux).",
            "schema": { "type": "number", "minimum": -90, "maximum": 90 },
            "example": 48.8566
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "description": "Longitude (WGS 84, degrés décimaux).",
            "schema": { "type": "number", "minimum": -180, "maximum": 180 },
            "example": 2.3522
          }
        ],
        "responses": {
          "200": {
            "description": "Adresse trouvée, ou `label` à null.",
            "headers": {
              "Cache-Control": { "schema": { "type": "string" }, "description": "public, s-maxage=86400, stale-while-revalidate=604800 (no-store sans réponse du géocodeur)" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ReverseResult" },
                "example": { "label": "8 Place de l'Hôtel de Ville-esplanade de la Libération 75004 Paris" }
              }
            }
          },
          "400": {
            "description": "lat ou lon absents ou invalides.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": { "error": "Paramètres lat et lon requis" }
              }
            }
          },
          "429": {
            "description": "Plus de 30 requêtes par minute depuis le même réseau.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReverseResult" }, "example": { "label": null } } }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DayHours": {
        "type": "array",
        "description": "Plages d'ouverture d'un jour au format `HH:MM-HH:MM`, heure de Paris. Tableau vide : fermé ce jour-là. `00:00-24:00` : ouvert toute la journée.",
        "items": { "type": "string", "pattern": "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$" },
        "examples": [["09:00-12:00", "14:00-19:00"], []]
      },
      "Point": {
        "type": "object",
        "description": "Point relais, consigne automatique ou bureau de poste. La fiche du point est à l'adresse https://www.relaisautourdemoi.fr/point/<nom>--<id> (le nom est décoratif : une adresse au nom périmé redirige vers la bonne).",
        "required": ["id", "carrier", "type", "name", "address", "postcode", "city", "lat", "lon", "source"],
        "properties": {
          "id": { "type": "string", "description": "Identifiant stable : `mr-…` (Mondial Relay), `lp-…` (La Poste), `osm-n…` (OpenStreetMap).", "examples": ["mr-fr012345", "lp-00001a"] },
          "carrier": {
            "type": "string",
            "description": "Réseau qui exploite le point.",
            "enum": ["mondial-relay", "la-poste", "pickup", "relais-colis", "ups", "dhl", "gls", "amazon", "vinted-go", "colis-prive", "autre"]
          },
          "type": { "type": "string", "enum": ["relay", "locker", "post"], "description": "Commerce relais, consigne automatique ou bureau de poste." },
          "name": { "type": "string" },
          "address": { "type": "string", "description": "Numéro et rue ; peut être vide." },
          "postcode": { "type": "string", "description": "Code postal ; peut être vide (seule la ville est connue)." },
          "city": { "type": "string" },
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "hours": { "type": "array", "description": "Semaine type, 7 jours (index 0 = lundi). Absent si inconnue.", "items": { "$ref": "#/components/schemas/DayHours" }, "minItems": 7, "maxItems": 7 },
          "exceptions": {
            "type": "array",
            "description": "Dates qui s'écartent de la semaine type (jours fériés, fermetures), connues pour La Poste.",
            "items": {
              "type": "object",
              "required": ["date", "hours"],
              "properties": { "date": { "type": "string", "format": "date" }, "hours": { "$ref": "#/components/schemas/DayHours" } }
            }
          },
          "h24": { "type": "boolean", "description": "Accessible 24h/24 et 7j/7." },
          "services": { "type": "array", "items": { "type": "string", "enum": ["pickup", "dropoff", "return"] }, "description": "Retrait, dépôt, retour." },
          "parking": {
            "type": "array",
            "description": "Stationnement le plus proche de chaque type à 200 m au plus (OpenStreetMap, incomplet).",
            "items": {
              "type": "object",
              "required": ["kind", "d"],
              "properties": {
                "kind": { "type": "string", "enum": ["lot", "shop", "street"], "description": "Parking public, parking clients, places en voirie." },
                "d": { "type": "number", "description": "Distance en mètres." },
                "fee": { "type": "boolean", "description": "Payant ; absent si inconnu." }
              }
            }
          },
          "parkingNote": { "type": "string", "description": "Stationnement décrit à la main, remplace `parking`." },
          "photo": { "type": "string", "description": "Identifiant de la photo officielle Mondial Relay." },
          "photoUrl": { "type": "string", "description": "Photo principale choisie par le site (adresse relative au site)." },
          "info": { "type": "string", "description": "Précision de la source (type d'établissement)." },
          "source": { "type": "string", "enum": ["inpost", "laposte", "osm", "live"] },
          "d": { "type": "number", "description": "Distance au point de recherche, en kilomètres (recherche autour d'un point seulement)." }
        }
      },
      "NearbyPoints": {
        "type": "object",
        "required": ["points", "radiusKm", "live"],
        "properties": {
          "points": { "type": "array", "items": { "$ref": "#/components/schemas/Point" } },
          "radiusKm": { "type": "number", "description": "Rayon de recherche retenu, en kilomètres." },
          "live": { "type": "array", "items": { "type": "string" }, "description": "Connecteurs temps réel des transporteurs qui ont complété la réponse." }
        }
      },
      "AreaPoints": {
        "type": "object",
        "required": ["points", "total", "truncated"],
        "properties": {
          "points": { "type": "array", "items": { "$ref": "#/components/schemas/Point" } },
          "total": { "type": "integer", "description": "Nombre de points de la zone." },
          "truncated": { "type": "boolean", "description": "Vrai si `points` n'est qu'un échantillon réparti sur la zone." }
        }
      },
      "Suggestion": {
        "type": "object",
        "required": ["label", "lat", "lon", "kind"],
        "properties": {
          "label": { "type": "string" },
          "sub": { "type": "string", "description": "Précision affichée sous le libellé (transporteur, adresse, distance, département)." },
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "kind": { "type": "string", "enum": ["city", "address", "street", "place", "point", "poi"] },
          "href": { "type": "string", "description": "Page de la ville ou fiche du point relais, adresse relative au site." }
        }
      },
      "SuggestionList": {
        "type": "object",
        "required": ["items"],
        "properties": { "items": { "type": "array", "maxItems": 8, "items": { "$ref": "#/components/schemas/Suggestion" } } }
      },
      "ReverseResult": {
        "type": "object",
        "required": ["label"],
        "properties": { "label": { "type": ["string", "null"], "description": "Adresse lisible, ou null." } }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      }
    }
  }
}
