Sous-régions
Catalogue ISO 3166-2 petit mais paginé (~99 documents aujourd’hui) hébergé dans l’index Meilisearch MEILISEARCH_SUBREGIONS_INDEX (défaut subregions). Lecture seule depuis Meili — aucun domaine Subregion côté MySQL, l’index est alimenté par un processus externe. Le pattern paginé est conservé par symétrie avec /api/regions et pour qu’une croissance du catalogue ne casse pas les clients.
Identité côté API : le id JSON:API d’une ressource subregions est le code ISO 3166-2 en minuscules (ag-10, ag-11, …). Les documents Meili stockent le code en majuscules dans le champ id (AG-10, AG-11) — Hydrogen normalise à la sérialisation. Les URLs sont case-insensitive end-to-end.
Hiérarchie : chaque sous-région porte country_id (ISO 3166-1 alpha-2 du pays parent, ex: AG) et region_id (ISO 3166-2 de la région parente, ex: AG-10). Les deux sont filtrables sur tous les endpoints listing/search.
Descriptions Markdown : la ressource détail (GET /api/subregions/{code}) embarque un attribut description dont la valeur est le contenu brut Markdown du fichier resources/lang/<locale>/subregions/<code>/description.md. Mêmes propriétés et même fallback que pour les pays / régions (locale courante puis SupportedLocales::DEFAULT, sinon null). Le Markdown n’est pas rendu côté serveur.
Blocs country + region inline : chaque ressource subregions (listing, search, détail) embarque deux sous-objets résolvant la hiérarchie complète sans appel supplémentaire client :
country— sous-ensemble léger du pays parent (idlowercase,name,slug,continent,continentId). Forme identique à celui exposé sur/api/regions.region— sous-ensemble léger de la région parente (idlowercase ISO 3166-2,name,slug,countryIdlowercase).
Côté serveur, deux appels Meili sont émis par requête HTTP quel que soit le nombre de hits (un sur l’index countries, un sur regions), via les batch-loaders CountrySummaryResolver / RegionSummaryResolver. country ou region retombent sur null quand le country_id / region_id est manquant ou ne résout pas (code orphelin). Pour les attributs riches d’une région ou d’un pays (description longue, native_name, alt_names…), passer par /api/regions/<code> / /api/countries/<code>.
Garde anti-path-traversal : le code passe par une regex
^[a-z0-9-]{2,7}$avant toute lecture disque.
GET /api/subregions
Section titled “GET /api/subregions”Listing paginé du catalogue, avec filtres optionnels par pays et/ou région parente.
-
Auth : aucune.
-
Action : ListSubregionsAction.
-
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.region(string, optionnel) — code ISO 3166-2 de la région parente (case-insensitive). Filtre Meili exact surregion_id. Format invalide →422 Invalid region code.- Combinables (filtre AND). Combiner
country+regionest typiquement redondant (la région implique son pays) mais accepté en défense en profondeur.
-
Pas de
descriptiondans le listing : charger les 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": "subregions", "id": "ag-10", "attributes": { "name": "Barbuda", "slug": "barbuda", "codes": { "osm": null, "wikidata": "Q238752", "wikipedia": "en:Barbuda" }, "names": { "en-US": "Barbuda", "fr-FR": "Barbuda" }, "official_names": null, "country_id": "AG", "region_id": "AG-10", "stats": { "stats": null, "surface": "144" }, "latitude": 17.62, "longitude": -61.78, "country": { "id": "ag", "name": "Antigua and Barbuda", "slug": "antigua-and-barbuda", "continent": "Americas", "continentId": "AM" }, "region": { "id": "ag-10", "name": "Barbuda", "slug": "barbuda", "countryId": "ag" } } } ], "links": { "self": "https://api.example/api/subregions?country=ag&limit=20", "first": "https://api.example/api/subregions?country=ag&limit=20", "prev": null, "next": null, "last": "https://api.example/api/subregions?country=ag&limit=20&offset=0" }, "meta": { "totalHits": 2, "limit": 20, "offset": 0, "count": 2, "country": "AG" }}- Réponses d’erreur :
422 Invalid country code—countryne matche pas[a-zA-Z]{2}.422 Invalid region code—regionne matche pas[a-zA-Z0-9-]{2,7}.503 Search backend unavailable.
GET /api/subregions/search
Section titled “GET /api/subregions/search”Quatre modes combinables :
- Full-text :
?q=<term>. - Geo-rayon :
?lat=&lng=&distance=(mètres) — les trois ensemble, sinon422. - Filtre pays :
?country=AG. - Filtre région :
?region=AG-10.
Tous combinés via AND (conjonction de filtres Meili).
-
Auth : aucune.
-
Action : SearchSubregionsAction.
-
Query :
q(string, optionnel) — recherche full-text.lat/lng/distance— tous trois requis ensemble pour le mode géo.country/region— comme sur le listing.limit(int, défaut20, plafond50).offset(int ≥ 0, défaut0).
-
Plafond distance :
SUBREGION_NEARBY_MAX_DISTANCE_METERS(défaut 1 000 000 m = 1 000 km). Dépassement →422 Distance too large. -
Tri géo : en mode géo, tri par
_geoPoint(lat, lng):asc. Pré-requis index satisfait par défaut (_geodanssortableAttributes). -
Réponse
200 OK: structure identique à/api/regions/searchmodulosubregionscommetypeet la présence deregion_iddans les attributs. Les blocscountryetregionsont présents sur chaque hit (voir intro de section). -
Réponses d’erreur :
422 Invalid country code/Invalid region code— formats malformés.422 Incomplete geo parameters— un deslat/lng/distancefourni mais pas les autres.422 Invalid latitude/Invalid longitude/Invalid distance.422 Distance too large—distance > SUBREGION_NEARBY_MAX_DISTANCE_METERS.503 Search backend unavailable.
GET /api/subregions/{code}
Section titled “GET /api/subregions/{code}”Détail d’une sous-région par son code ISO 3166-2 (case-insensitive). Filtre Meili exact sur id = "<UPPER>".
-
Auth : aucune.
-
Action : GetSubregionAction.
-
Path :
{code}— regex[a-zA-Z0-9-]{2,7}. Casse normalisée à la résolution. Format invalide →404.
-
Différence clé avec le listing : ajoute un attribut
descriptionchargé depuisresources/lang/<locale>/subregions/<code>/description.md. -
Réponse
200 OK(localefr-FR) :
{ "data": { "type": "subregions", "id": "ag-10", "attributes": { "name": "Barbuda", "slug": "barbuda", "codes": { "osm": null, "wikidata": "Q238752", "wikipedia": "en:Barbuda" }, "names": { "en-US": "Barbuda", "fr-FR": "Barbuda" }, "official_names": null, "country_id": "AG", "region_id": "AG-10", "stats": { "stats": null, "surface": "144" }, "latitude": 17.62, "longitude": -61.78, "country": { "id": "ag", "name": "Antigua and Barbuda", "slug": "antigua-and-barbuda", "continent": "Americas", "continentId": "AM" }, "region": { "id": "ag-10", "name": "Barbuda", "slug": "barbuda", "countryId": "ag" }, "description": "# Barbuda\n\n**Barbuda** est l'une des deux îles principales…" } }}- Réponses d’erreur :
404 Subregion not found— code invalide OU aucun document Meili correspondant.503 Search backend unavailable.
Pré-requis index Meilisearch (one-shot ops) — tous déjà actifs sur le déploiement :
id,country_id,region_id,_geo,namedansfilterableAttributes_geodanssortableAttributes- les champs texte recherchables dans
searchableAttributes(*couvre tout)