---
title: "Sous-régions"
description: "Catalogue ISO 3166-2 petit mais paginé (~99 documents aujourd'hui) hébergé dans l'index Meilisearch MEILISEARCH_SUBREGIONS_INDEX (défaut subregions). Lectu…"
---

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.

## `GET /api/subregions`

Listing paginé du catalogue, avec filtres optionnels par pays et/ou région parente.

- **Auth** : aucune.
- **Action** : [ListSubregionsAction](../src/Http/Action/Api/Subregion/ListSubregionsAction.php).
- **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`** :

```json
{
  "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` — `country` ne matche pas `[a-zA-Z]{2}`.
  - `422 Invalid region code` — `region` ne matche pas `[a-zA-Z0-9-]{2,7}`.
  - `503 Search backend unavailable`.

## `GET /api/subregions/search`

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](../src/Http/Action/Api/Subregion/SearchSubregionsAction.php).
- **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 large` — `distance > SUBREGION_NEARBY_MAX_DISTANCE_METERS`.
  - `503 Search backend unavailable`.

## `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](../src/Http/Action/Api/Subregion/GetSubregionAction.php).
- **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`) :

```json
{
  "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)

---
