{
  "openapi": "3.1.0",
  "info": {
    "title": "API des marchés de Marchés autour de moi",
    "version": "1.0.0",
    "summary": "Marchés de France autour d'un point, dans une zone ou par identifiant, avec leurs jours, horaires et prochaines dates.",
    "description": "API publique en lecture seule, sans clé, du site https://www.marchesautourdemoi.fr. Elle renvoie les marchés (alimentaires, couverts, bio, de producteurs, du soir, brocantes) avec leur semaine type et leurs prochaines dates calculées pour les 15 jours à venir (heure de Paris, jours fériés compris, vacances scolaires non gérées).\n\nTrois façons d'interroger, par ordre de priorité si plusieurs paramètres sont fournis : `ids` (liste d'identifiants), puis `bbox` (zone rectangulaire), puis `lat` et `lon` (autour d'un point, du plus proche au plus éloigné).\n\nDonnées : OpenStreetMap (ODbL 1.0, attribution « © les contributeurs d'OpenStreetMap » obligatoire, partage à l'identique des bases dérivées) et données ouvertes de la Ville de Paris (ODbL), de la Métropole de Lyon, de Toulouse Métropole, de Clermont Auvergne Métropole et d'Orléans Métropole (Licence Ouverte 2.0 d'Etalab, mention de la source). Les horaires officiels priment sur ceux d'OpenStreetMap. Les horaires peuvent changer (été, jours fériés, intempéries) : vérifiez auprès de la mairie.\n\nRéponses en cache une heure (`Cache-Control: public, s-maxage=3600`). Merci de rester raisonnable : quelques requêtes par seconde au plus.",
    "contact": {
      "name": "Marchés autour de moi",
      "url": "https://www.marchesautourdemoi.fr/a-propos",
      "email": "contact@marchesautourdemoi.fr"
    },
    "license": {
      "name": "ODbL 1.0 (données OpenStreetMap et Ville de Paris) et Licence Ouverte 2.0 (autres données publiques)",
      "url": "https://opendatacommons.org/licenses/odbl/1-0/"
    }
  },
  "externalDocs": {
    "description": "Sources et méthode",
    "url": "https://www.marchesautourdemoi.fr/a-propos"
  },
  "servers": [{ "url": "https://www.marchesautourdemoi.fr" }],
  "paths": {
    "/api/markets": {
      "get": {
        "operationId": "listMarkets",
        "summary": "Marchés autour d'un point, dans une zone ou par identifiant",
        "description": "Autour d'un point : `lat` et `lon` obligatoires, avec `radius` (rayon fixe) ou `near` (rayon élargi automatiquement). Dans une zone : `bbox`. Par identifiant : `ids`. Les marchés sans jours connus sont inclus (semaine entièrement à null et `next` vide).",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "description": "Latitude du point de recherche (WGS 84, degrés décimaux). Obligatoire sans `bbox` ni `ids`.",
            "schema": { "type": "number", "minimum": -90, "maximum": 90 },
            "example": 45.764
          },
          {
            "name": "lon",
            "in": "query",
            "description": "Longitude du point de recherche (WGS 84, degrés décimaux). Obligatoire sans `bbox` ni `ids`.",
            "schema": { "type": "number", "minimum": -180, "maximum": 180 },
            "example": 4.8357
          },
          {
            "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": 10 }
          },
          {
            "name": "near",
            "in": "query",
            "description": "Toute valeur non vide (par exemple `1`) active la recherche des plus proches : rayon de 5 km, doublé jusqu'à 40 km tant que moins de 12 marchés sont trouvés. Le rayon retenu est rendu dans `radiusKm`.",
            "schema": { "type": "string" },
            "example": "1"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre maximal de marchés renvoyés. Autour d'un point : 1 à 300, 150 par défaut. Avec `bbox` : 1 à 2 500, 1 500 par défaut (échantillon régulier au-delà).",
            "schema": { "type": "integer", "minimum": 1, "maximum": 2500 }
          },
          {
            "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": "4.75,45.70,4.95,45.82"
          },
          {
            "name": "ids",
            "in": "query",
            "description": "Identifiants de marchés séparés par des virgules (100 au plus, les inconnus sont ignorés). Un identifiant est la partie après `--` dans l'adresse d'une fiche `/marche/<nom>--<id>`.",
            "schema": { "type": "string" },
            "example": "osm-n2911180061,paris-16"
          },
          {
            "name": "d",
            "in": "query",
            "description": "Date du jour (AAAA-MM-JJ), sans effet sur le résultat : sert seulement à ne pas réutiliser d'un jour à l'autre une réponse gardée en cache. Les dates sont toujours calculées à partir d'aujourd'hui, heure de Paris.",
            "schema": { "type": "string", "format": "date" }
          }
        ],
        "responses": {
          "200": {
            "description": "Marchés trouvés. Autour d'un point, ils sont triés du plus proche au plus éloigné et portent `d` ; la réponse donne aussi `radiusKm`. Avec `bbox`, elle donne `total` et `truncated`.",
            "headers": {
              "Cache-Control": { "schema": { "type": "string" }, "description": "public, s-maxage=3600, stale-while-revalidate=3600" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/MarketList" },
                "example": {
                  "markets": [
                    {
                      "id": "osm-n2911180061",
                      "name": "Marché Saint-Antoine",
                      "tags": [],
                      "address": "Quai Saint-Antoine",
                      "postcode": "69002",
                      "city": "Lyon 2e",
                      "insee": "69382",
                      "lat": 45.761525,
                      "lon": 4.831339,
                      "week": [null, ["06:00-13:00"], ["06:00-13:00"], ["06:00-13:00"], ["06:00-13:30"], ["06:00-13:30"], ["06:00-14:00"]],
                      "d": 0.436,
                      "next": [
                        { "date": "2026-10-20", "ranges": ["06:00-13:00"] },
                        { "date": "2026-10-21", "ranges": ["06:00-13:00"] },
                        { "date": "2026-10-22", "ranges": ["06:00-13:00"] }
                      ]
                    }
                  ],
                  "radiusKm": 5
                }
              }
            }
          },
          "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" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DayRanges": {
        "type": "array",
        "description": "Plages horaires d'un jour au format `HH:MM-HH:MM`, heure de Paris. Tableau vide : marché ce jour-là, horaires non précisés.",
        "items": { "type": "string", "pattern": "^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$" },
        "examples": [["07:00-13:00"], ["08:00-12:30", "16:00-19:00"], []]
      },
      "Session": {
        "type": "object",
        "description": "Une date de marché.",
        "required": ["date", "ranges"],
        "properties": {
          "date": { "type": "string", "format": "date", "description": "Jour du marché (AAAA-MM-JJ, heure de Paris)." },
          "ranges": {
            "description": "Plages horaires de ce jour, ou null quand les horaires ne sont pas précisés.",
            "oneOf": [{ "$ref": "#/components/schemas/DayRanges" }, { "type": "null" }]
          }
        }
      },
      "Market": {
        "type": "object",
        "required": ["id", "name", "tags", "address", "postcode", "city", "insee", "lat", "lon", "week", "next"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifiant stable : `osm-n…`, `osm-w…`, `osm-r…` (objet OpenStreetMap) ou préfixé par la collectivité source (`paris-16`, `lyon-…`).",
            "examples": ["osm-n2911180061"]
          },
          "name": { "type": "string" },
          "tags": {
            "type": "array",
            "description": "Types de marché. `soir` : marché qui commence à 15 h ou plus tard.",
            "items": { "type": "string", "enum": ["couvert", "producteurs", "bio", "brocante", "soir"] }
          },
          "address": { "type": "string", "description": "Adresse ou lieu (peut être vide)." },
          "postcode": { "type": "string" },
          "city": { "type": "string" },
          "insee": { "type": "string", "description": "Code INSEE de la commune, ou de l'arrondissement à Paris, Lyon et Marseille." },
          "lat": { "type": "number" },
          "lon": { "type": "number" },
          "week": {
            "type": "array",
            "description": "Semaine type sur 7 entrées, **lundi en premier** (index 0 = lundi, 6 = dimanche). `null` : pas de marché ce jour-là. Résumé des jours où le marché se tient au moins une semaine active sur trois ; les dates exactes sont dans `next`.",
            "minItems": 7,
            "maxItems": 7,
            "items": { "oneOf": [{ "$ref": "#/components/schemas/DayRanges" }, { "type": "null" }] }
          },
          "months": {
            "type": "array",
            "description": "Mois d'activité (1 à 12), seulement pour un marché saisonnier.",
            "items": { "type": "integer", "minimum": 1, "maximum": 12 }
          },
          "monthly": { "type": "boolean", "description": "Marché mensuel (environ une fois par mois) : `week` donne alors le jour, pas une fréquence." },
          "d": {
            "type": "number",
            "description": "Distance à vol d'oiseau en kilomètres depuis `lat`/`lon` (arrondie au mètre). Seulement pour une recherche autour d'un point."
          },
          "next": {
            "type": "array",
            "description": "Prochaines dates du marché sur les 15 jours à venir, aujourd'hui compris (heure de Paris), avec leurs horaires. Vide si les jours sont inconnus ou si le marché n'a pas lieu pendant cette période.",
            "items": { "$ref": "#/components/schemas/Session" }
          }
        }
      },
      "MarketList": {
        "type": "object",
        "required": ["markets"],
        "properties": {
          "markets": { "type": "array", "items": { "$ref": "#/components/schemas/Market" } },
          "radiusKm": { "type": "number", "description": "Rayon de recherche retenu, en kilomètres (recherche autour d'un point)." },
          "total": { "type": "integer", "description": "Nombre de marchés dans la zone (avec `bbox`)." },
          "truncated": { "type": "boolean", "description": "Avec `bbox` : vrai si `total` dépasse le nombre de marchés renvoyés." }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      }
    }
  }
}
