Skip to content

Régions

Catalogue ISO 3166-2 non borné (~5 300 documents) hébergé dans l’index Meilisearch MEILISEARCH_REGIONS_INDEX (défaut regions). Lecture seule depuis Meili — aucun domaine Region côté MySQL, l’index est alimenté par un processus externe. Contrairement aux pays, le volume impose une pagination obligatoire (les 5 300 entrées ne tiennent pas dans un seul appel raisonnable).

Identité côté API : le id JSON:API d’une ressource regions est le code ISO 3166-2 en minuscules (fr-idf, ad-02, gb-eng, …). Les documents Meili stockent le code en majuscules dans le champ id (FR-IDF, AD-02) — Hydrogen normalise à la sérialisation. Les URLs sont case-insensitive end-to-end (/api/regions/fr-idf/api/regions/FR-IDF).

Descriptions Markdown : la ressource détail (GET /api/regions/{code}) embarque un attribut description dont la valeur est le contenu brut Markdown du fichier resources/lang/<locale>/regions/<code>/description.md. Le layout est un dossier par région (<code>/) contenant description.md — mêmes propriétés et même fallback que pour les pays. La locale est résolue par LocaleResolverMiddleware (header Accept-Language ou param). Si le fichier n’existe pas pour la locale courante, fallback sur la locale SupportedLocales::DEFAULT ; si aucune des deux n’existe, description = null (la région sert quand même, juste sans blurb). Le Markdown n’est pas rendu côté serveur.

Bloc country inline : chaque ressource regions (listing, search, détail) embarque un attribut country qui est un sous-ensemble léger du document pays parent — assez d’info pour afficher “Île-de-France · France · Europe” sans nécessiter un appel à /api/countries/<code>. Champs exposés : id (ISO alpha-2 minuscule, ex: fr), name, slug, continent, continentId. Pour la description longue, la liste des fuseaux ou les official_name multi-locales, appeler /api/countries/<code>. Le bloc résout en un seul appel Meili sur l’index countries par requête (batch-loader request-scoped), peu importe le nombre de hits. Si le country_id d’une région est manquant ou ne résout pas (code orphelin), country = null.

Garde anti-path-traversal : le code passe par une regex ^[a-z0-9-]{2,7}$ avant toute lecture disque. Toute tentative d’injection court-circuite la résolution → description = null.

Listing paginé du catalogue, avec filtre optionnel par pays.

  • Auth : aucune.

  • Action : ListRegionsAction.

  • Query :

    • limit (int, défaut 20, plafond 100).
    • offset (int ≥ 0, défaut 0).
    • country (string, optionnel) — code ISO 3166-1 alpha-2 (2 lettres, case-insensitive). Filtre Meili exact sur country_id. Format invalide → 422 Invalid country code.
  • Pas de description dans le listing : charger des milliers de fichiers Markdown sur un endpoint de catalogue est gaspilleur. Le blurb n’est disponible que sur le détail.

  • Réponse 200 OK :

{
"data": [
{
"type": "regions",
"id": "fr-idf",
"attributes": {
"name": "Île-de-France",
"slug": "ile-de-france",
"type": "Région",
"codes": { "osm": "8649", "wikidata": "Q13917", "wikipedia": "fr:Île-de-France" },
"names": { "fr-FR": "Île-de-France", "en-US": "Île-de-France" },
"native_name": "Île-de-France",
"alt_names": null,
"official_names": null,
"country_id": "FR",
"stats": { "stats": null, "surface": "12011.4" },
"latitude": 48.85,
"longitude": 2.35,
"country": {
"id": "fr",
"name": "France",
"slug": "france",
"continent": "Europe",
"continentId": "EU"
}
}
}
],
"links": {
"self": "https://api.example/api/regions?country=fr&limit=20",
"first": "https://api.example/api/regions?country=fr&limit=20",
"prev": null,
"next": "https://api.example/api/regions?country=fr&limit=20&offset=20",
"last": "https://api.example/api/regions?country=fr&limit=20&offset=0"
},
"meta": {
"totalHits": 18,
"limit": 20,
"offset": 0,
"count": 18,
"country": "FR"
}
}
  • Réponses d’erreur :
    • 422 Invalid country codecountry ne matche pas la regex [a-zA-Z]{2}.
    • 503 Search backend unavailable.

Trois modes combinables :

  1. Full-text : ?q=<term>.
  2. Geo-rayon : ?lat=&lng=&distance= (mètres) — les trois ensemble, sinon 422.
  3. Filtre pays : ?country=FR — combinable avec n’importe quel autre mode.
  • Auth : aucune.

  • Action : SearchRegionsAction.

  • Query :

    • q (string, optionnel) — recherche full-text.
    • lat (float [-90, 90]), lng (float [-180, 180]), distance (int positif, mètres) — tous trois requis ensemble pour le mode géo, sinon 422 Incomplete geo parameters.
    • country (string, optionnel) — ISO 3166-1 alpha-2.
    • limit (int, défaut 20, plafond 50).
    • offset (int ≥ 0, défaut 0).
  • Plafond distance : REGION_NEARBY_MAX_DISTANCE_METERS (défaut 2 000 000 m = 2 000 km). Dépassement → 422 Distance too large.

  • Tri géo : en mode géo, tri par _geoPoint(lat, lng):asc (distance croissante). En mode full-text pur, tri par pertinence Meili. Pré-requis ops : _geo dans sortableAttributes de l’index regions (NON activé par défaut sur le déploiement, voir bloc Pré-requis en fin de section).

  • Réponse 200 OK :

{
"data": [
{
"type": "regions",
"id": "fr-idf",
"attributes": {
"name": "Île-de-France",
"country_id": "FR",
"country": { "id": "fr", "name": "France", "slug": "france", "continent": "Europe", "continentId": "EU" },
"...": "..."
}
}
],
"links": {
"self": "https://api.example/api/regions/search?q=ile",
"first": "https://api.example/api/regions/search?q=ile&limit=20",
"prev": null,
"next": null,
"last": null
},
"meta": {
"totalHits": 4,
"limit": 20,
"offset": 0,
"query": "ile",
"country": "FR",
"center": { "lat": 48.85, "lng": 2.35 },
"distance": 500000
}
}

country, center et distance ne sont présents que quand le mode correspondant est actif.

  • Réponses d’erreur :
    • 422 Invalid country codecountry mal formé.
    • 422 Incomplete geo parameters — un des lat/lng/distance fourni mais pas les autres.
    • 422 Invalid latitude / Invalid longitude / Invalid distance — valeurs hors bornes ou non numériques.
    • 422 Distance too largedistance > REGION_NEARBY_MAX_DISTANCE_METERS.
    • 503 Search backend unavailable.

Détail d’une région par son code ISO 3166-2 (2 à 7 caractères, case-insensitive). Filtre Meili exact sur id = "<UPPER>" — pas de recherche full-text (le scorer pourrait surfacer la mauvaise subdivision sur des codes de bord).

  • Auth : aucune.

  • Action : GetRegionAction.

  • Path :

    • {code} — regex [a-zA-Z0-9-]{2,7} (accepte XX, XX-YYY, XX-99, …). Casse normalisée à la résolution (majuscules pour Meili, minuscules pour le id JSON:API). Format invalide → 404.
  • Différence clé avec le listing : ajoute un attribut description chargé depuis resources/lang/<locale>/regions/<code>/description.md (voir intro de section).

  • Réponse 200 OK (locale fr-FR) :

{
"data": {
"type": "regions",
"id": "fr-idf",
"attributes": {
"name": "Île-de-France",
"slug": "ile-de-france",
"type": "Région",
"codes": { "osm": "8649", "wikidata": "Q13917", "wikipedia": "fr:Île-de-France" },
"names": { "fr-FR": "Île-de-France", "en-US": "Île-de-France" },
"native_name": "Île-de-France",
"alt_names": null,
"official_names": null,
"country_id": "FR",
"stats": { "stats": null, "surface": "12011.4" },
"latitude": 48.85,
"longitude": 2.35,
"country": { "id": "fr", "name": "France", "slug": "france", "continent": "Europe", "continentId": "EU" },
"description": "# Île-de-France\n\nL'**Île-de-France** est la région la plus peuplée…"
}
}
}
  • Réponses d’erreur :
    • 404 Region not found — code invalide (regex non matchée) OU code valide mais aucun document Meili correspondant.
    • 503 Search backend unavailable.

Pré-requis index Meilisearch (one-shot ops) :

  • id dans filterableAttributes (pour le lookup exact du détail) — déjà actif.
  • country_id dans filterableAttributes (pour le filtre ?country=) — déjà actif.
  • _geo dans filterableAttributes (pour ?lat=&lng=&distance=) — déjà actif.
  • _geo dans sortableAttributes (pour trier par distance dans le mode géo) — NON activé par défaut ; sans lui, le filtre _geoRadius(...) fonctionne mais le tri par distance retombe sur l’ordre de pertinence Meili.
  • les champs texte recherchables dans searchableAttributes (* couvre tout sur le déploiement courant).