{
    "openapi": "3.1.0",
    "info": {
        "title": "Wellpack Core API \u2014 Comptage",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "http://localhost/api"
        }
    ],
    "security": [
        {
            "http": []
        }
    ],
    "paths": {
        "/oauth/token": {
            "post": {
                "operationId": "accessToken.issueToken",
                "summary": "Issue an access token",
                "tags": [
                    "AccessToken"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/geo/stores": {
            "post": {
                "operationId": "geo.findStores",
                "description": "Decision (scope enforcement on the optional Google driver): the route\nstays under the single geo:resolve scope for every driver.\nAuthenticateCountClient validates the Bearer and its scope but never\nexposes the resulting AccessToken past the middleware (no request\nattribute is set), so gating Google behind an additional geo:geocode\nscope check here would mean re-running the whole OAuth token\nvalidation a second time in the controller \u2014 that duplication was\njudged worse than the simpler guard below. The only gate for Google\nis therefore its config flag: geo:resolve remains sufficient and\nrequired in every case.",
                "summary": "Find the physical outlets of a brand across a bounded list of cities\n(max 50 \u2014 see FindStoresRequest), aggregating and deduplicating\nresults across cities (dedup key: name + address, case-insensitive)",
                "tags": [
                    "FindStores"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/FindStoresRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Array of `StoreResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/StoreResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "error": {
                                            "type": "string",
                                            "const": "stores_google_disabled"
                                        }
                                    },
                                    "required": [
                                        "error"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/v1/geo/geocode": {
            "post": {
                "operationId": "geo.geocode",
                "description": "Two passes: first a parallel BAN-only batch (BanDriver::geocodeBatch,\nHttp::pool) for latency, then a sequential cascade through the full\nGeocoder (cache + per-provider rate limits + other providers) for\nwhatever the batch pass missed. The BAN batch pass bypasses the\nmanager's cache \u2014 acceptable since BAN is free and effectively\nrate-unlimited at this volume, and it never touches the manager's\ncascade for other providers; the manager stays the single semantic\nentry point for the fallback path.\n\nEvery match found is then enriched from the LOCAL IRIS reference only\n(never from a geocoding provider): GeoResolver::locatePoint() returns\nnull without error when the point falls outside every known contour or\nthe reference is empty. A miss on one address never fails the batch.",
                "summary": "Geocode a bounded batch of addresses (max 25 \u2014 see GeocodeRequest)",
                "tags": [
                    "Geocode"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/GeocodeRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Array of `GeocodeResultResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/GeocodeResultResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/v1/ops/health": {
            "get": {
                "operationId": "health",
                "description": "S\u00e9pare la vitalit\u00e9 du service (sondes critiques qu'il poss\u00e8de : DB, cl\u00e9s\nPassport) de la sant\u00e9 de ses d\u00e9pendances externes (joignabilit\u00e9 du moteur\nWepak, en information). HTTP 200 tant que la vitalit\u00e9 est verte \u2014 m\u00eame si\nune d\u00e9pendance tierce est en d\u00e9faut ; HTTP 503 seulement si une sonde\ncritique tombe. Un al\u00e9a r\u00e9seau c\u00f4t\u00e9 Wepak ne requalifie donc pas la brique\nen Unhealthy pour les sondes de la plateforme.",
                "summary": "\u00c9tat de sant\u00e9 applicatif de la brique",
                "tags": [
                    "Health"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/geo/communes": {
            "post": {
                "operationId": "geo.matchCommunes",
                "description": "Le rapprochement est int\u00e9gralement local (recherche pg_trgm sur le\nr\u00e9f\u00e9rentiel), donc un lot large (max 200 \u2014 voir MatchCommunesRequest) ne\nd\u00e9clenche aucun appel externe. Chaque couple est r\u00e9solu ind\u00e9pendamment :\nun couple introuvable est signal\u00e9 par `error: not_found` sans jamais\ninvalider le lot. La r\u00e9ponse est donc toujours 200 ; seule une validation\nde payload invalide renvoie 422.",
                "summary": "Rapproche un lot de couples (ville, code postal) de communes INSEE",
                "tags": [
                    "MatchCommunes"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/MatchCommunesRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Array of `CommuneMatchResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/CommuneMatchResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/v1/counts/{id}": {
            "get": {
                "operationId": "pollCount",
                "description": "`status` vaut `running` tant que le comptage court, `done` quand le\nr\u00e9sultat est pos\u00e9, `failed` sur \u00e9chec du moteur ou d\u00e9passement du d\u00e9lai.\n`volume` et `zones` sont nuls hors de `done`. `expires_at` reprend\nl'\u00e9ch\u00e9ance annonc\u00e9e au 202, identique \u00e0 chaque relev\u00e9 : pass\u00e9 cet\ninstant, `status` vaut toujours `done` ou `failed`. La r\u00e9ponse est plate :\n`status`, `volume`, `zones` et `expires_at` \u00e0 la racine, sans enveloppe.\nR\u00e9ponses d'erreur : 404 pour un job inconnu comme pour le job d'un autre\nclient, sans distinction.",
                "summary": "Relever l'\u00e9tat d'un comptage",
                "tags": [
                    "PollCount"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "status": {
                                            "type": "string"
                                        },
                                        "volume": {
                                            "type": [
                                                "integer",
                                                "null"
                                            ]
                                        },
                                        "zones": {
                                            "type": [
                                                "array",
                                                "null"
                                            ],
                                            "items": {}
                                        },
                                        "expires_at": {
                                            "type": "string",
                                            "format": "date-time"
                                        }
                                    },
                                    "required": [
                                        "status",
                                        "volume",
                                        "zones",
                                        "expires_at"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string",
                                            "const": "Unknown job."
                                        }
                                    },
                                    "required": [
                                        "message"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/v1/geo/zones": {
            "post": {
                "operationId": "geo.resolveZones",
                "description": "R\u00e9ponses d'erreur : 422 si la validation du payload \u00e9choue (zones/coordonn\u00e9es\ninvalides), 503 si le r\u00e9f\u00e9rentiel IRIS est vide (jamais import\u00e9) ou indisponible.\nUne zone en temps de trajet dont l'isochrone est indisponible ne fait pas\n\u00e9chouer le lot : elle sort avec un champ `error` propre \u00e0 cette zone.",
                "summary": "R\u00e9soudre un lot de zones (cercles et/ou temps de trajet) contre le\nr\u00e9f\u00e9rentiel IRIS",
                "tags": [
                    "ResolveZones"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ResolveZonesRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "`ResolvedZoneSetResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/ResolvedZoneSetResource"
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "error": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "error"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/v1/counts": {
            "post": {
                "operationId": "submitCount",
                "description": "Le comptage est mis en file : la r\u00e9ponse 202 porte `job_id`, `status` et\n`expires_at`, jamais de volume. `expires_at` est l'\u00e9ch\u00e9ance annonc\u00e9e :\npass\u00e9 cet instant, le poll rend toujours un statut final. L'en-t\u00eate\n`Location` donne l'URL de poll (GET /counts/{id}) o\u00f9 le r\u00e9sultat sera\nrelev\u00e9. R\u00e9ponses d'erreur : 422 si la validation du payload \u00e9choue ; 429\nsi le client a d\u00e9j\u00e0 trop de comptages en vol (code count_limit_reached).",
                "summary": "Soumettre un comptage",
                "tags": [
                    "SubmitCount"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/CountRequest"
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "Pattern async-request-reply : le 202 porte l'URL de poll en Location.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "job_id": {
                                            "type": "integer"
                                        },
                                        "status": {
                                            "type": "string",
                                            "const": "running"
                                        },
                                        "expires_at": {
                                            "type": "string",
                                            "format": "date-time"
                                        }
                                    },
                                    "required": [
                                        "job_id",
                                        "status",
                                        "expires_at"
                                    ]
                                }
                            }
                        },
                        "headers": {
                            "Location": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "429 et non 422 : le payload est valide, c'est le debit du client\nqui ne l'est pas, et la soumission redeviendra acceptable sans\nrien changer. `code` distingue ce refus du throttle de route.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string",
                                            "const": "Too many counts in flight for this client: poll or wait for the running ones to complete."
                                        },
                                        "code": {
                                            "type": "string",
                                            "const": "count_limit_reached"
                                        }
                                    },
                                    "required": [
                                        "message",
                                        "code"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "http": {
                "type": "http",
                "scheme": "bearer"
            }
        },
        "schemas": {
            "Channel": {
                "type": "string",
                "enum": [
                    "sms",
                    "email",
                    "postal",
                    "telemarketing"
                ],
                "title": "Channel"
            },
            "CommuneMatchResource": {
                "type": "object",
                "additionalProperties": {},
                "title": "CommuneMatchResource"
            },
            "CountRequest": {
                "type": "object",
                "properties": {
                    "channel": {
                        "description": "Canal de diffusion vise par le comptage.",
                        "example": "sms",
                        "$ref": "#/components/schemas/Channel"
                    },
                    "audience": {
                        "type": "object",
                        "description": "Criteres de ciblage de l'audience (tous optionnels).",
                        "example": {
                            "gender": "F",
                            "age_min": 25,
                            "age_max": 49
                        },
                        "properties": {
                            "gender": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "enum": [
                                    "M",
                                    "F"
                                ]
                            },
                            "age_min": {
                                "type": [
                                    "integer",
                                    "null"
                                ],
                                "minimum": 18,
                                "maximum": 100
                            },
                            "age_max": {
                                "type": [
                                    "integer",
                                    "null"
                                ],
                                "minimum": 18,
                                "maximum": 100
                            },
                            "interests": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "mode": {
                                            "$ref": "#/components/schemas/InterestMode"
                                        },
                                        "interest_ids": {
                                            "type": "array",
                                            "items": {
                                                "type": "integer"
                                            },
                                            "maxItems": 500
                                        },
                                        "qualif_ids": {
                                            "type": "array",
                                            "items": {
                                                "type": "integer"
                                            },
                                            "maxItems": 500
                                        }
                                    },
                                    "required": [
                                        "mode"
                                    ]
                                },
                                "maxItems": 50
                            },
                            "interest_id": {
                                "type": [
                                    "integer",
                                    "null"
                                ]
                            },
                            "qualification_ids": {
                                "type": "array",
                                "items": {
                                    "type": "integer"
                                },
                                "maxItems": 500
                            },
                            "interest_groups": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "id": {
                                            "type": "integer"
                                        },
                                        "mode": {
                                            "$ref": "#/components/schemas/InterestMode"
                                        },
                                        "name": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "maxLength": 100
                                        }
                                    },
                                    "required": [
                                        "id",
                                        "mode"
                                    ]
                                },
                                "maxItems": 50
                            },
                            "has_email": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "has_phone": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "rcs_capable": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "has_birth_date": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "normalized_address": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "phone_type": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 30
                            },
                            "recency": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 30
                            }
                        }
                    },
                    "suppressions": {
                        "type": "object",
                        "description": "Listes de suppression a exclure du comptage (repoussoirs).",
                        "example": {
                            "campaign_ids": [
                                "12345"
                            ],
                            "static_enabled": true
                        },
                        "properties": {
                            "file_id": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 100
                            },
                            "campaign_ids": {
                                "type": "array",
                                "items": {
                                    "type": "string",
                                    "maxLength": 50
                                },
                                "maxItems": 100
                            },
                            "upload_id": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 100
                            },
                            "static_enabled": {
                                "type": "boolean"
                            },
                            "external_id": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 100
                            }
                        }
                    },
                    "options": {
                        "type": "object",
                        "description": "Options de calcul du volume.",
                        "example": {
                            "deduplicate": true,
                            "b2b": false
                        },
                        "properties": {
                            "deduplicate": {
                                "type": "boolean"
                            },
                            "excluded_carriers": {
                                "type": "array",
                                "items": {
                                    "type": "integer"
                                },
                                "maxItems": 50
                            },
                            "included_carriers": {
                                "type": "array",
                                "items": {
                                    "type": "integer"
                                },
                                "maxItems": 50
                            },
                            "max_volume": {
                                "type": [
                                    "integer",
                                    "null"
                                ],
                                "minimum": 1
                            },
                            "exclude_traps": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            },
                            "b2b": {
                                "type": [
                                    "boolean",
                                    "null"
                                ]
                            }
                        }
                    },
                    "context": {
                        "type": "object",
                        "description": "Contexte applicatif, a des fins de tracabilite (non filtrant).",
                        "example": {
                            "partner": "wellpack",
                            "advertiser": "demo"
                        },
                        "properties": {
                            "application_id": {
                                "type": [
                                    "integer",
                                    "null"
                                ]
                            },
                            "partner": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 100
                            },
                            "advertiser": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "maxLength": 100
                            }
                        }
                    },
                    "zones": {
                        "type": "array",
                        "description": "Zones geographiques ciblees. Chaque zone porte SOIT `code` (une seule)\nSOIT `codes` (plusieurs), jamais les deux. Le format du code depend du\n`type` (dept: 1-3 chiffres ou 2A/2B, cp: 5 chiffres, insee: 5 chiffres ou\n2A/2B + 3 chiffres, iris: 9 chiffres ou 2A/2B + 7 chiffres ; 2A/2B dans\nles deux casses, transmis tels quels). `codes` : 4000 codes au plus.",
                        "example": [
                            {
                                "type": "dept",
                                "code": "77"
                            }
                        ],
                        "items": {
                            "type": "object",
                            "properties": {
                                "type": {
                                    "$ref": "#/components/schemas/ZoneType"
                                },
                                "code": {
                                    "type": "string"
                                },
                                "codes": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    },
                                    "minItems": 1,
                                    "maxItems": 4000
                                },
                                "label": {
                                    "type": "string",
                                    "maxLength": 100
                                },
                                "max_volume": {
                                    "type": "integer",
                                    "minimum": 1
                                },
                                "refine": {
                                    "type": "object",
                                    "properties": {
                                        "type": {
                                            "type": "string",
                                            "enum": [
                                                "insee"
                                            ]
                                        },
                                        "codes": {
                                            "type": "array",
                                            "items": {
                                                "type": "string",
                                                "pattern": "^(\\d{5}|2[ABab]\\d{3})$"
                                            },
                                            "minItems": 1,
                                            "maxItems": 4000
                                        }
                                    }
                                }
                            },
                            "required": [
                                "type"
                            ]
                        },
                        "maxItems": 5000
                    }
                },
                "title": "CountRequest"
            },
            "FindStoresRequest": {
                "type": "object",
                "properties": {
                    "brand": {
                        "type": "string",
                        "description": "Enseigne recherch\u00e9e.",
                        "example": "Carrefour",
                        "maxLength": 100
                    },
                    "driver": {
                        "type": "string",
                        "description": "Driver de recherche. D\u00e9faut : config('geo.stores.default_driver').",
                        "enum": [
                            "sirene",
                            "google"
                        ]
                    },
                    "cities": {
                        "type": "array",
                        "description": "Villes dans lesquelles chercher, born\u00e9es \u00e0 50. Les r\u00e9sultats sont\nagr\u00e9g\u00e9s et d\u00e9doublonn\u00e9s entre les villes (voir FindStoresController).",
                        "example": [
                            "Meaux",
                            "Amiens"
                        ],
                        "items": {
                            "type": "string",
                            "maxLength": 200
                        },
                        "minItems": 1,
                        "maxItems": 50
                    }
                },
                "required": [
                    "brand",
                    "cities"
                ],
                "title": "FindStoresRequest"
            },
            "GeocodeRequest": {
                "type": "object",
                "properties": {
                    "addresses": {
                        "type": "array",
                        "description": "Adresses \u00e0 g\u00e9ocoder, born\u00e9es \u00e0 25 par lot (voir GeocodeController).",
                        "example": [
                            {
                                "query": "8 bd du port amiens",
                                "postcode": "80000"
                            }
                        ],
                        "items": {
                            "type": "object",
                            "properties": {
                                "query": {
                                    "type": "string",
                                    "maxLength": 255
                                },
                                "postcode": {
                                    "type": "string",
                                    "minLength": 5,
                                    "maxLength": 5
                                }
                            },
                            "required": [
                                "query"
                            ]
                        },
                        "minItems": 1,
                        "maxItems": 25
                    }
                },
                "required": [
                    "addresses"
                ],
                "title": "GeocodeRequest"
            },
            "GeocodeResultResource": {
                "type": "object",
                "additionalProperties": {},
                "title": "GeocodeResultResource"
            },
            "InterestMode": {
                "type": "string",
                "enum": [
                    "include",
                    "include_any",
                    "exclude"
                ],
                "title": "InterestMode"
            },
            "MatchCommunesRequest": {
                "type": "object",
                "properties": {
                    "communes": {
                        "type": "array",
                        "description": "Couples (ville, code postal) \u00e0 rapprocher d'une commune INSEE.\nLot born\u00e9 \u00e0 200 : le rapprochement est 100 % local (Postgres).",
                        "example": [
                            {
                                "city": "Marseille",
                                "postal_code": "13001"
                            }
                        ],
                        "items": {
                            "type": "object",
                            "properties": {
                                "city": {
                                    "type": "string",
                                    "maxLength": 200
                                },
                                "postal_code": {
                                    "type": "string",
                                    "minLength": 5,
                                    "maxLength": 5
                                }
                            },
                            "required": [
                                "city",
                                "postal_code"
                            ]
                        },
                        "minItems": 1,
                        "maxItems": 200
                    }
                },
                "required": [
                    "communes"
                ],
                "title": "MatchCommunesRequest"
            },
            "ResolveZonesRequest": {
                "type": "object",
                "properties": {
                    "min_coverage_pct": {
                        "type": "number",
                        "description": "Seuil minimal de couverture (%) sous lequel un IRIS est \u00e9cart\u00e9 du r\u00e9sultat.",
                        "example": 5,
                        "minimum": 0,
                        "maximum": 100
                    },
                    "include_geometries": {
                        "type": "boolean",
                        "description": "Inclure les g\u00e9om\u00e9tries GeoJSON de chaque IRIS r\u00e9solu."
                    },
                    "exclude_codes": {
                        "type": "array",
                        "description": "Codes IRIS \u00e0 exclure du r\u00e9sultat (ex. campagne d\u00e9j\u00e0 adress\u00e9e). Format : 9 chiffres pour la m\u00e9tropole/DOM, OU un code corse\n`2A`/`2B` suivi de 7 chiffres (ex. `2A0010000`). La casse est\nstricte (majuscules), le r\u00e9f\u00e9rentiel IGN ne stockant les codes\ncorses qu'en majuscules \u2014 pas de drapeau `i` sur la regex.",
                        "items": {
                            "type": "string",
                            "pattern": "^(\\d{9}|2[AB]\\d{7})$"
                        },
                        "maxItems": 5000
                    },
                    "zones": {
                        "type": "array",
                        "description": "Zones \u00e0 r\u00e9soudre contre le r\u00e9f\u00e9rentiel IRIS. Chaque zone est SOIT\nun cercle (`radius_m`) SOIT une zone en temps de trajet\n(`travel_time_min` + `profile` optionnel), jamais les deux.",
                        "example": [
                            {
                                "label": "Magasin Paris",
                                "lat": 48.8566,
                                "lon": 2.3522,
                                "radius_m": 500
                            },
                            {
                                "label": "10 min voiture",
                                "lat": 45.764,
                                "lon": 4.8357,
                                "travel_time_min": 10,
                                "profile": "car"
                            }
                        ],
                        "items": {
                            "type": "object",
                            "properties": {
                                "label": {
                                    "type": "string",
                                    "maxLength": 100
                                },
                                "lat": {
                                    "type": "number",
                                    "minimum": -90,
                                    "maximum": 90
                                },
                                "lon": {
                                    "type": "number",
                                    "minimum": -180,
                                    "maximum": 180
                                },
                                "radius_m": {
                                    "type": "integer",
                                    "description": "Rayon du cercle en m\u00e8tres. Exclusif avec `travel_time_min`.",
                                    "minimum": 50,
                                    "maximum": 50000
                                },
                                "travel_time_min": {
                                    "type": "integer",
                                    "description": "Temps de trajet maximal en minutes. Exclusif avec `radius_m`.",
                                    "minimum": 2,
                                    "maximum": 60
                                },
                                "profile": {
                                    "type": "string",
                                    "description": "Mode de d\u00e9placement pour une zone en temps de trajet (d\u00e9faut `car`).",
                                    "enum": [
                                        "car",
                                        "pedestrian"
                                    ]
                                }
                            },
                            "required": [
                                "label",
                                "lat",
                                "lon"
                            ]
                        },
                        "minItems": 1,
                        "maxItems": 50
                    }
                },
                "required": [
                    "zones"
                ],
                "title": "ResolveZonesRequest"
            },
            "ResolvedZoneSetResource": {
                "type": "object",
                "properties": {
                    "zones": {
                        "type": "array",
                        "items": {}
                    },
                    "combined": {
                        "type": "object",
                        "properties": {
                            "type": {
                                "type": "string",
                                "const": "iris"
                            },
                            "codes": {
                                "type": "array",
                                "items": {}
                            },
                            "population_covered": {
                                "type": [
                                    "integer",
                                    "null"
                                ]
                            }
                        },
                        "required": [
                            "type",
                            "codes",
                            "population_covered"
                        ]
                    }
                },
                "required": [
                    "zones",
                    "combined"
                ],
                "title": "ResolvedZoneSetResource"
            },
            "StoreResource": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "address": {
                        "type": "string"
                    },
                    "lat": {
                        "type": "string"
                    },
                    "lon": {
                        "type": "string"
                    },
                    "source": {
                        "type": "string"
                    },
                    "city_query": {
                        "type": "string"
                    }
                },
                "required": [
                    "name",
                    "address",
                    "lat",
                    "lon",
                    "source",
                    "city_query"
                ],
                "title": "StoreResource"
            },
            "ZoneType": {
                "type": "string",
                "enum": [
                    "dept",
                    "cp",
                    "insee",
                    "iris",
                    "commune"
                ],
                "title": "ZoneType"
            }
        },
        "responses": {
            "ValidationException": {
                "description": "Validation error",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "description": "Errors overview."
                                },
                                "errors": {
                                    "type": "object",
                                    "description": "A detailed description of each field that failed validation.",
                                    "additionalProperties": {
                                        "type": "array",
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            },
                            "required": [
                                "message",
                                "errors"
                            ]
                        }
                    }
                }
            }
        }
    }
}