Skip to content

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 (id lowercase, name, slug, continent, continentId). Forme identique à celui exposé sur /api/regions.
  • region — sous-ensemble léger de la région parente (id lowercase ISO 3166-2, name, slug, countryId lowercase).

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.

Listing paginé du catalogue, avec filtres optionnels par pays et/ou région parente.

  • Auth : aucune.

  • Action : ListSubregionsAction.

  • 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.
    • region (string, optionnel) — code ISO 3166-2 de la région parente (case-insensitive). Filtre Meili exact sur region_id. Format invalide → 422 Invalid region code.
    • Combinables (filtre AND). Combiner country + region est typiquement redondant (la région implique son pays) mais accepté en défense en profondeur.
  • Pas de description dans 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 codecountry ne matche pas [a-zA-Z]{2}.
    • 422 Invalid region coderegion ne matche pas [a-zA-Z0-9-]{2,7}.
    • 503 Search backend unavailable.

Quatre modes combinables :

  1. Full-text : ?q=<term>.
  2. Geo-rayon : ?lat=&lng=&distance= (mètres) — les trois ensemble, sinon 422.
  3. Filtre pays : ?country=AG.
  4. 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éfaut 20, plafond 50).
    • offset (int ≥ 0, défaut 0).
  • 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 (_geo dans sortableAttributes).

  • Réponse 200 OK : structure identique à /api/regions/search modulo subregions comme type et la présence de region_id dans les attributs. Les blocs country et region sont 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 des lat/lng/distance fourni mais pas les autres.
    • 422 Invalid latitude / Invalid longitude / Invalid distance.
    • 422 Distance too largedistance > SUBREGION_NEARBY_MAX_DISTANCE_METERS.
    • 503 Search backend unavailable.

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 description chargé depuis resources/lang/<locale>/subregions/<code>/description.md.

  • Réponse 200 OK (locale fr-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, name dans filterableAttributes
  • _geo dans sortableAttributes
  • les champs texte recherchables dans searchableAttributes (* couvre tout)