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.
GET /api/regions
Section titled “GET /api/regions”Listing paginé du catalogue, avec filtre optionnel par pays.
-
Auth : aucune.
-
Action : ListRegionsAction.
-
Query :
limit(int, défaut20, plafond100).offset(int ≥ 0, défaut0).country(string, optionnel) — code ISO 3166-1 alpha-2 (2 lettres, case-insensitive). Filtre Meili exact surcountry_id. Format invalide →422 Invalid country code.
-
Pas de
descriptiondans 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 code—countryne matche pas la regex[a-zA-Z]{2}.503 Search backend unavailable.
GET /api/regions/search
Section titled “GET /api/regions/search”Trois modes combinables :
- Full-text :
?q=<term>. - Geo-rayon :
?lat=&lng=&distance=(mètres) — les trois ensemble, sinon422. - 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, sinon422 Incomplete geo parameters.country(string, optionnel) — ISO 3166-1 alpha-2.limit(int, défaut20, plafond50).offset(int ≥ 0, défaut0).
-
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 :_geodanssortableAttributesde l’indexregions(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,centeretdistancene sont présents que quand le mode correspondant est actif.
- Réponses d’erreur :
422 Invalid country code—countrymal formé.422 Incomplete geo parameters— un deslat/lng/distancefourni mais pas les autres.422 Invalid latitude/Invalid longitude/Invalid distance— valeurs hors bornes ou non numériques.422 Distance too large—distance > REGION_NEARBY_MAX_DISTANCE_METERS.503 Search backend unavailable.
GET /api/regions/{code}
Section titled “GET /api/regions/{code}”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}(accepteXX,XX-YYY,XX-99, …). Casse normalisée à la résolution (majuscules pour Meili, minuscules pour leidJSON:API). Format invalide →404.
-
Différence clé avec le listing : ajoute un attribut
descriptionchargé depuisresources/lang/<locale>/regions/<code>/description.md(voir intro de section). -
Réponse
200 OK(localefr-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) :
iddansfilterableAttributes(pour le lookup exact du détail) — déjà actif.country_iddansfilterableAttributes(pour le filtre?country=) — déjà actif._geodansfilterableAttributes(pour?lat=&lng=&distance=) — déjà actif._geodanssortableAttributes(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).