Points d'intérêt
Triplet /api/pois (list paginé + facettes) / /api/pois/search (full-text + géo + facettes) / /api/pois/{id} (détail) adossé à l’index Meilisearch MEILISEARCH_POIS_INDEX (défaut pois). Catalogue ~210 000 documents dérivés d’OpenStreetMap ⇒ pagination obligatoire sur les listings.
Comme les établissements, Hydrogen n’a pas de domaine Poi côté MySQL : l’index Meilisearch est la source de vérité, alimenté par un ETL externe. Pas de re-hydratation SQL, pas de cache applicatif — chaque hit Meili devient ressource JSON:API directement.
Identité côté API : le id JSON:API d’une ressource pois est le hash hex 32 caractères en minuscules (UUID sans tirets). Les documents source stockent le id en majuscules (AED604B47ADD11F196D500155DDA08DE) ; on normalise vers le lowercase pour l’URL/JSON:API et l’endpoint de détail remet en majuscules avant d’interroger l’index — l’espace d’URL reste insensible à la casse de bout en bout (GET /api/pois/aed6… = GET /api/pois/AED6…).
Shape attributes commune (formatter partagé) : les 3 endpoints utilisent PoiHitFormatter, qui :
- recopie tous les champs du document dans
attributes, sauf les clés Meili-internes (_geo,_geoDistance,_formatted,_matchesPosition,_rankingScore,_rankingScoreDetails) et leidbrut (passe en JSON:API id) ; - aplatit
_geo: { lat, lng }enattributes.latitude/attributes.longitude; - expose
_geoDistance(mètres) enattributes.distanceMetersquand le tri géo est actif (search en mode géo uniquement).
Champs métier exposés tels quels depuis l’index : osm_id, osm_type, name, alt_names[], names{}, category, subcategories[], address, postcode, la hiérarchie country_id/region_id/subregion_id/city_id + leurs libellés dénormalisés country/region/subregion/city, importance, has_geo, attributes{} (tags OSM bruts), et les champs optionnels opening_hours, phone, website, wikidata, wikipedia. Leur format est défini par le pipeline d’alimentation, pas par Hydrogen.
Pas de description Markdown ni d’assets statiques ici — contrairement aux pays/régions/sous-régions (blurb éditorial) ou aux établissements (carousel
images+ description), les POIs sont dérivés d’OpenStreetMap et ne portent aucun asset on-disk : le document Meili est la ressource entière.
Facettes communes (list + search)
Section titled “Facettes communes (list + search)”Les deux endpoints de listing partagent le même jeu de filtres, cumulables (composés en AND côté Meili) et validés à l’identique via PoiFilters. Chaque valeur est normalisée à la casse stockée dans l’index avant d’être injectée dans le filtre :
| Query | Champ Meili | Format attendu | Exemple |
|---|---|---|---|
category | category | slug minuscule ([a-z0-9_-]{1,50}) | transport |
country | country_id | ISO 3166-1 alpha-2 | fr |
region | region_id | ISO 3166-2 | fr-ara |
subregion | subregion_id | ISO 3166-2 | fr-38 |
city | city_id | hex 32 | 676584c2… |
hasGeo | has_geo | true / false (1 / 0) | true |
Toute valeur mal formée renvoie un 422 ciblé (Invalid category / Invalid country code / Invalid region code / Invalid subregion code / Invalid city id / Invalid hasGeo) avec le pointer correspondant. Les filtres appliqués sont ré-échoés (normalisés) dans meta.
GET /api/pois
Section titled “GET /api/pois”Listing paginé du catalogue, sans filtre full-text ni géo (pour ça, voir /api/pois/search), avec les facettes ci-dessus.
-
Auth : aucune.
-
Action : ListPoisAction.
-
Query :
limit(int, défaut20, plafond100).offset(int ≥ 0, défaut0).sort(string, optionnel) — whitelist :importance/-importance(= défaut, desc),importance_asc,name(asc),-name(desc). Valeur hors whitelist →422 Invalid sort.- facettes :
category,country,region,subregion,city,hasGeo(cf. section précédente).
-
Tri par défaut :
importance:desc— les POIs les plus notables en haut. -
Réponse
200 OK:
{ "data": [ { "type": "pois", "id": "aed604b47add11f196d500155dda08de", "attributes": { "osm_id": 136221, "osm_type": "node", "name": "Chavant", "alt_names": [], "names": { "default": "Chavant" }, "category": "transport", "subcategories": ["tramway_station"], "address": null, "postcode": null, "country_id": "FR", "region_id": "FR-ARA", "subregion_id": "FR-38", "city_id": "676584C252B711F196D500155DDA08DE", "country": "France", "region": "Auvergne-Rhône-Alpes", "subregion": "Isère", "city": "Grenoble", "importance": 0.0, "has_geo": true, "attributes": { "access": { "tram": "yes", "railway": "tram_stop" } }, "latitude": 45.1845327, "longitude": 5.7317282 } } ], "links": { "self": "https://api.example/api/pois?category=transport&limit=20", "first": "https://api.example/api/pois?category=transport&limit=20", "prev": null, "next": "https://api.example/api/pois?category=transport&limit=20&offset=20", "last": "https://api.example/api/pois?category=transport&limit=20&offset=980" }, "meta": { "totalHits": 12043, "limit": 20, "offset": 0, "count": 20, "sort": "importance:desc", "category": "transport" }}- Réponses d’erreur :
422 Invalid sort— valeur desorthors whitelist.422 Invalid …— valeur de facette mal formée (cf. section Facettes).503 Search backend unavailable.
GET /api/pois/search
Section titled “GET /api/pois/search”Recherche de POIs par nom (?q=…), par proximité GPS (?lat=…&lng=…&distance=…), par facettes, ou toute combinaison des trois.
-
Auth : aucune (endpoint public).
-
Action : SearchPoisAction.
-
Query :
q(optionnel) — recherche full-text (searchable :name,alt_names,names,city,category,subcategories,region,country,address,postcode). Sansq, Meilisearch retourne tous les documents (filtrés par facette/géo si présent).lat,lng,distance— tous les trois ou aucun. Fournir l’un sans les autres ⇒422 Incomplete geo parameters. Avec eux, le filtre_geoRadius(lat, lng, distance)s’applique ET le tri bascule sur_geoPoint(lat, lng):asc(du plus proche au plus éloigné).lat: float dans[-90, 90].lng: float dans[-180, 180].distance: entier positif (mètres), borné parPOI_NEARBY_MAX_DISTANCE_METERS(défaut 50 km). Au-delà →422 Distance too large.
- facettes :
category,country,region,subregion,city,hasGeo(cumulables avecqet/ou le géo). limit(1..50, défaut 20).offset(≥0, défaut 0).
-
Réponse
200 OK:
{ "data": [ { "type": "pois", "id": "aed604b47add11f196d500155dda08de", "attributes": { "name": "Chavant", "category": "transport", "subcategories": ["tramway_station"], "country_id": "FR", "city": "Grenoble", "importance": 0.0, "has_geo": true, "latitude": 45.1845327, "longitude": 5.7317282, "distanceMeters": 214.8 } } ], "links": { "self": "https://api.example/api/pois/search?q=chavant&lat=45.18&lng=5.73&distance=2000&limit=20", "first": "https://api.example/api/pois/search?q=chavant&lat=45.18&lng=5.73&distance=2000&limit=20", "prev": null, "next": null, "last": null }, "meta": { "totalHits": 3, "limit": 20, "offset": 0, "query": "chavant", "category": "transport", "center": { "lat": 45.18, "lng": 5.73 }, "distance": 2000 }}-
meta.totalHits: estimé par Meilisearch (sémantique offset). -
meta.centeretmeta.distancene sont présents que si le mode géo est actif. -
attributes.distanceMetersn’est présent que sur les hits issus d’un tri_geoPoint:asc(mode géo). -
Les clés de facettes (
category,country, …) ne sont présentes dansmetaque si le filtre correspondant a été fourni. -
Pagination : offset-based, navigation via
links.{self,first,prev,next,last}. -
Réponses d’erreur :
422 Incomplete geo parameters— un seul delat/lng/distanceest fourni (ou deux sur trois).422 Invalid latitude/Invalid longitude/Invalid distance— valeurs hors plage ou non numériques.422 Distance too large—distance>POI_NEARBY_MAX_DISTANCE_METERS.422 Invalid …— valeur de facette mal formée.503 Search backend unavailable— Meilisearch injoignable, index manquant, ou pré-requis index non satisfaits. Le détail Meili est propagé danserrors[0].detail.
Pré-requis index Meilisearch (ops one-shot) :
_geodansfilterableAttributesETsortableAttributes(mode géo)category,subcategories,country_id,region_id,subregion_id,city_id,has_geodansfilterableAttributes(facettes)importance,namedanssortableAttributes(tri du listing)- champs textuels (
name,alt_names, …) danssearchableAttributes
GET /api/pois/{id}
Section titled “GET /api/pois/{id}”Détail d’un POI par son id hex.
-
Auth : aucune.
-
Action : GetPoiAction.
-
Path :
{id}— regex[a-fA-F0-9]{32}(insensible à la casse). L’action met en majuscules pour interroger l’index (ids stockés upper) et renvoie le JSON:API id en minuscules.
-
Résolution : filtre Meili exact
id = "<HEX_UPPER>"sur l’index (l’attributidest filterable) — pas de recherche full-text. -
Réponse
200 OK:
{ "data": { "type": "pois", "id": "aed604b47add11f196d500155dda08de", "attributes": { "osm_id": 136221, "osm_type": "node", "name": "Chavant", "category": "transport", "subcategories": ["tramway_station"], "country_id": "FR", "region_id": "FR-ARA", "subregion_id": "FR-38", "city_id": "676584C252B711F196D500155DDA08DE", "country": "France", "region": "Auvergne-Rhône-Alpes", "subregion": "Isère", "city": "Grenoble", "importance": 0.0, "has_geo": true, "attributes": { "access": { "tram": "yes", "railway": "tram_stop" } }, "latitude": 45.1845327, "longitude": 5.7317282 } }}- Réponses d’erreur :
404 POI not found— id mal formé (n’atteint pas l’action, le router 404e via la regex) OU id valide mais absent de l’index.503 Search backend unavailable.