# language: fr @api @search @geolocation @mvp Fonctionnalité: Recherche géographique de contenus avec Nominatim En tant qu'utilisateur de RoadWave Je veux rechercher des contenus audio dans une zone géographique spécifique En utilisant un nom de lieu (ville, monument, région) et un rayon de recherche Afin de découvrir des contenus avant de me déplacer ou planifier un trajet Contexte: Étant donné un utilisateur authentifié Et l'API Nominatim est accessible # ============================================================================ # GEOCODAGE AVEC NOMINATIM (lieu → coordonnées GPS) # ============================================================================ Scénario: Recherche par nom de ville (cas simple) Étant donné l'utilisateur saisit "Paris" dans la recherche géographique Quand le système interroge Nominatim avec la requête "Paris, France" Alors Nominatim doit retourner les coordonnées : | lat | 48.8566 | | lon | 2.3522 | | type | city | | bbox | (48.815, 48.902, 2.224, 2.470) | Et le système doit utiliser ces coordonnées comme centre de recherche Scénario: Recherche par monument ou POI (Point of Interest) Étant donné l'utilisateur saisit "Tour Eiffel" Quand le système interroge Nominatim Alors Nominatim doit retourner : | lat | 48.8584 | | lon | 2.2945 | | display_name| Tour Eiffel, Paris, France | | type | tourism | Et ces coordonnées doivent être utilisées pour la recherche de contenus Scénario: Recherche avec ambiguïté (plusieurs résultats) Étant donné l'utilisateur saisit "Montmartre" Quand le système interroge Nominatim Alors plusieurs résultats doivent être retournés : | display_name | lat | lon | | Montmartre, Paris 18e, France | 48.8867 | 2.3431 | | Montmartre, Saskatchewan, Canada | 49.3000 | -106.0 | | Montmartre-de-Bretagne, Ille-et-Vilaine | 48.1867 | -1.1833| Et l'utilisateur doit choisir dans une liste déroulante Et le choix par défaut doit être le résultat français si détecté Scénario: Recherche avec contexte géographique (biais local) Étant donné l'utilisateur est actuellement à Lyon (GPS actif) Et l'utilisateur saisit "Bellecour" Quand le système interroge Nominatim avec viewbox="Lyon area" Alors Nominatim doit prioriser les résultats proches de Lyon Et "Place Bellecour, Lyon" doit apparaître en premier Avant "Bellecour, Jura" ou d'autres homonymes Scénario: Recherche par code postal Étant donné l'utilisateur saisit "75001" Quand le système interroge Nominatim Alors Nominatim doit retourner le centre du 1er arrondissement de Paris Et un rayon par défaut de 1km doit être appliqué Scénario: Recherche invalide ou introuvable Étant donné l'utilisateur saisit "Azertyuiopqsdfghjklm" Quand le système interroge Nominatim Alors Nominatim doit retourner 0 résultat Et un message d'erreur doit être affiché : """ Aucun lieu trouvé pour "Azertyuiopqsdfghjklm". Essayez un nom de ville, monument ou adresse. """ # ============================================================================ # RAYON DE RECHERCHE CONFIGURABLE # ============================================================================ Scénario: Recherche avec rayon par défaut (5 km) Étant donné l'utilisateur cherche "Louvre, Paris" Et Nominatim retourne les coordonnées (48.8606, 2.3376) Et aucun rayon n'est spécifié Quand le système effectue la recherche de contenus Alors un rayon par défaut de 5 km doit être appliqué Et une requête PostGIS doit être exécutée : """ SELECT * FROM contents WHERE ST_DWithin(location::geography, ST_MakePoint(2.3376, 48.8606)::geography, 5000) """ Scénario: Recherche avec rayon personnalisé (slider 1-50 km) Étant donné l'utilisateur cherche "Lyon" Et l'utilisateur ajuste le slider de rayon à 15 km Quand le système effectue la recherche de contenus Alors une requête PostGIS avec rayon 15000m doit être exécutée Et tous les contenus dans un rayon de 15 km autour de Lyon doivent être retournés Scénario: Rayon minimum (1 km) - recherche ultra locale Étant donné l'utilisateur cherche "Place de la Concorde" Et l'utilisateur définit le rayon à 1 km (minimum) Quand le système effectue la recherche Alors seuls les contenus très proches (<1 km) doivent être retournés Et le nombre de résultats peut être très faible (0-10) Scénario: Rayon maximum (50 km) - recherche large Étant donné l'utilisateur cherche "Versailles" Et l'utilisateur définit le rayon à 50 km (maximum) Quand le système effectue la recherche Alors tous les contenus dans un rayon de 50 km doivent être retournés Et cela peut inclure Paris, banlieue et zones périphériques Et un avertissement "Résultats nombreux, affinez votre recherche" peut s'afficher si >500 résultats Scénario: Mise à jour temps réel du rayon avec slider Étant donné l'utilisateur visualise les résultats pour "Bordeaux" avec rayon 10 km Quand l'utilisateur ajuste le slider de 10 km à 20 km Alors une nouvelle requête API doit être déclenchée automatiquement Et les résultats doivent être mis à jour en temps réel Et la carte (si affichée) doit ajuster le cercle de rayon # ============================================================================ # FILTRES COMBINÉS AVEC RECHERCHE GÉO # ============================================================================ Scénario: Recherche géo + filtre catégorie Étant donné l'utilisateur cherche "Marseille" avec rayon 10 km Et l'utilisateur filtre par catégorie "Tourisme" Quand le système effectue la recherche Alors seuls les contenus touristiques dans le rayon doivent être retournés Et la requête SQL doit combiner : """ WHERE ST_DWithin(...) AND category_id IN (SELECT id FROM categories WHERE name = 'Tourisme') """ Scénario: Recherche géo + filtre type de contenu Étant donné l'utilisateur cherche "Strasbourg" avec rayon 5 km Et l'utilisateur filtre par type "Audio-guides" Quand le système effectue la recherche Alors seuls les audio-guides dans le rayon doivent être retournés Et les podcasts et radios live doivent être exclus Scénario: Recherche géo + filtre durée Étant donné l'utilisateur cherche "Nice" avec rayon 15 km Et l'utilisateur filtre par durée "Court (<10 min)" Quand le système effectue la recherche Alors seuls les contenus de <10 minutes dans le rayon doivent être retournés Scénario: Recherche géo + mode Kids actif Étant donné l'utilisateur cherche "Disneyland Paris" avec rayon 5 km Et le mode Kids est activé (utilisateur 13-15 ans) Quand le système effectue la recherche Alors seuls les contenus "Tout public" doivent être retournés Et les contenus 16+ et 18+ doivent être exclus automatiquement Scénario: Recherche géo + filtre politique désactivé Étant donné l'utilisateur cherche "Assemblée Nationale" avec rayon 2 km Et l'utilisateur a désactivé les contenus politiques dans ses préférences Quand le système effectue la recherche Alors les contenus taggés "politique" doivent être exclus Même s'ils sont géographiquement pertinents # ============================================================================ # AFFICHAGE DES RÉSULTATS # ============================================================================ Scénario: Résultats triés par distance croissante Étant donné l'utilisateur cherche "Arc de Triomphe" avec rayon 3 km Quand les résultats sont retournés Alors ils doivent être triés par distance croissante : | contenu | distance | | Balade sur les Champs | 0.1 km | | Histoire de l'Arc | 0.2 km | | Quartier de l'Étoile | 0.5 km | | Secrets du 16e | 2.8 km | Et la distance doit être affichée pour chaque résultat Scénario: Affichage carte avec marqueurs de contenus Étant donné l'utilisateur cherche "Toulouse" avec rayon 10 km Quand les résultats sont affichés en mode "Carte" Alors une carte Leaflet doit être affichée Et un marqueur doit être placé pour chaque contenu : | contenu | lat | lon | icone | | Capitole de Toulouse | 43.6045 | 1.4442 | pin-tourisme | | Bords de Garonne | 43.5986 | 1.4330 | pin-nature | Et un cercle représentant le rayon de 10 km doit être affiché Et le centre doit être le point géocodé de Toulouse Scénario: Clustering de marqueurs si résultats nombreux Étant donné l'utilisateur cherche "Paris" avec rayon 20 km Et la recherche retourne 350 contenus Quand la carte est affichée Alors les marqueurs proches doivent être groupés en clusters : | cluster_center | nb_contenus | | Centre Paris | 120 | | La Défense | 45 | | Bois de Vincennes | 18 | Et en cliquant sur un cluster, le zoom doit s'approcher Et révéler les marqueurs individuels Scénario: Affichage liste + carte simultanés (vue hybride) Étant donné l'utilisateur cherche "Nantes" avec rayon 8 km Quand l'utilisateur active la vue "Hybride" Alors la liste de résultats doit s'afficher à gauche (60% écran) Et la carte doit s'afficher à droite (40% écran) Et en cliquant sur un résultat dans la liste, le marqueur doit être highlighté sur la carte Et vice-versa # ============================================================================ # PERFORMANCES & OPTIMISATIONS # ============================================================================ Scénario: Cache résultats recherche géo fréquente (Paris, Lyon, Marseille) Étant donné l'utilisateur cherche "Paris" avec rayon 5 km Quand la requête est exécutée pour la 1ère fois Alors les résultats doivent être mis en cache Redis pendant 10 minutes Et les requêtes suivantes identiques doivent utiliser le cache Et un header "X-Cache: HIT" doit être retourné Scénario: Index PostGIS pour performances requêtes spatiales Étant donné la table "contents" contient 100 000 contenus Quand une recherche géo est exécutée avec ST_DWithin Alors un index GIST sur la colonne "location" doit être utilisé Et le temps de réponse doit être <100ms (p95) Scénario: Pagination résultats nombreux Étant donné l'utilisateur cherche "Île-de-France" avec rayon 50 km Et la recherche retourne 1200 contenus Quand les résultats sont affichés Alors seuls les 50 premiers résultats doivent être chargés initialement Et un scroll infini doit charger les résultats suivants par batch de 50 Et le total "1200 contenus trouvés" doit être affiché en haut # ============================================================================ # EDGE CASES & ERREURS # ============================================================================ Scénario: Nominatim API indisponible (fallback) Étant donné l'utilisateur cherche "Lille" Quand l'API Nominatim retourne une erreur 503 (service unavailable) Alors un message d'erreur doit être affiché : """ Le service de recherche géographique est temporairement indisponible. Veuillez réessayer dans quelques instants. """ Et l'utilisateur doit pouvoir utiliser la recherche textuelle classique Scénario: Recherche géo sans connexion Internet Étant donné l'utilisateur est en mode offline Quand l'utilisateur tente une recherche géographique Alors un message doit indiquer : """ La recherche géographique nécessite une connexion Internet. Vous pouvez parcourir les contenus téléchargés. """ Scénario: Rate limiting Nominatim (1 req/s) Étant donné Nominatim impose un rate limit de 1 requête/seconde Et l'utilisateur tape rapidement "Par" → "Pari" → "Paris" Quand le système détecte plusieurs requêtes rapides Alors un debounce de 500ms doit être appliqué Et seule la dernière requête ("Paris") doit être envoyée à Nominatim Scénario: Recherche avec caractères spéciaux ou injection SQL Étant donné l'utilisateur saisit "Paris'; DROP TABLE contents; --" Quand le système traite la requête Alors les caractères spéciaux doivent être échappés Et aucune injection SQL ne doit être possible Et Nominatim doit retourner 0 résultat (lieu invalide) # ============================================================================ # SAUVEGARDE DES RECHERCHES (max 5) # ============================================================================ Scénario: Sauvegarde automatique d'une recherche géo Étant donné l'utilisateur effectue une recherche "Bordeaux" avec rayon 10 km Quand la recherche est validée Alors elle doit être sauvegardée dans l'historique : | search_query | Bordeaux | | radius_km | 10 | | lat | 44.8378 | | lon | -0.5792 | | timestamp | 2026-02-03 14:30:00 | Et l'utilisateur peut la rappeler via "Recherches récentes" Scénario: Limite de 5 recherches sauvegardées (FIFO) Étant donné l'utilisateur a déjà 5 recherches sauvegardées Quand l'utilisateur effectue une 6ème recherche "Montpellier" Alors la recherche la plus ancienne doit être supprimée Et "Montpellier" doit être ajoutée en 1ère position Et la limite de 5 recherches doit être respectée Scénario: Rejouer une recherche sauvegardée Étant donné l'utilisateur a une recherche sauvegardée "Lyon - 15 km" Quand l'utilisateur clique sur cette recherche dans l'historique Alors la recherche doit être ré-exécutée avec les mêmes paramètres Et les résultats actualisés doivent être affichés Et le rayon slider doit être positionné à 15 km # ============================================================================ # MÉTRIQUES & ANALYTICS # ============================================================================ Scénario: Logging des recherches géographiques pour analytics Étant donné l'utilisateur effectue une recherche "Grenoble" avec rayon 12 km Quand la recherche est exécutée Alors un événement analytics doit être loggé : | event_type | geo_search_executed | | search_query | Grenoble | | radius_km | 12 | | results_count | 87 | | response_time_ms | 145 | | cache_hit | false | | user_id | user-123 | | timestamp | 2026-02-03 14:30:00 | Et ces données doivent alimenter le dashboard de monitoring Scénario: Top recherches géographiques (analytics) Étant donné le système analyse les recherches sur 30 jours Quand le dashboard analytics est consulté Alors les lieux les plus recherchés doivent être affichés : | lieu | nb_recherches | | Paris | 12450 | | Lyon | 5632 | | Marseille | 4521 | | Toulouse | 3890 | | Nice | 3124 | Et ces données doivent guider la création de contenus ciblés