# Ka Maps — Synchronisation liste ↔ carte (le modèle Groupe-KA) Référence du module « recherche synchronisée » introduit avec Lou-Ka (`frontend/src/search/MapSearch.tsx`), conçu pour être décliné tel quel sur **immo-ka**, **resto-ka**, **sorti-ka** et servir de contrat à l'app iOS KA. ## Le principe : une seule vérité partagée La liste et la carte sont **deux vues du même état de recherche** : ``` État = { filtres, zone (bbox visible OU polygone dessiné), tri, page } ``` Chaque changement d'état déclenche **UNE requête** vers l'endpoint unifié (`/api/search` chez Lou-Ka) qui renvoie **dans la même réponse** : | Champ | Rôle | |----------------|------| | `total` | compteur partagé — affiché en tête de liste, égal PAR CONSTRUCTION au nombre de points carte | | `listings` | la page de liste demandée (objets complets) | | `points` | TOUS les points carte au format compact `[uid, lng, lat, prix, verdict]`, **triés comme la liste** | | `unpositioned` | annonces filtrées sans coordonnées (affichées honnêtement, jamais silencieusement perdues) | Corollaires structurels (pas des efforts de synchronisation, des invariants) : - compteur liste = nombre de marqueurs, dans 100 % des cas ; - `index d'un uid dans points` ÷ `page_size` = sa page de liste → un clic sur n'importe quel marqueur peut TOUJOURS faire défiler la liste vers l'annonce, même si elle est sur une autre page ; - aucune requête périmée ne peut écraser une récente (numéro de séquence + `AbortController` ; le serveur n'est jamais la source du tri d'arrivée). ## Répartition des rôles **Ka Maps (framework)** fournit : - `KaMap.fitBounds(bbox)` / `fitToProperties()` — recadrages animés marqués *programmés* ; - `moveend { byUser }` — distinction geste utilisateur / mouvement du code, c'est la clé du « respect de l'intention » : seuls les gestes verrouillent la vue (`userLocked`) et déclenchent la recherche par zone ; - sélection & survol bidirectionnels par feature-state GPU (`select(uid, "map"|"app")`, `setHovered`, événements `select`/`hover`) ; - outil polygone intégré (`startDraw`, `clearDrawnPolygon`, `setDrawnPolygon`, événement `draw`) + `` ; - fourchette de prix des clusters au survol (`clusterHover`, accumulateurs `valueMin`/`valueMax`) ; - état « vu » (`setSeenIds`) — pastilles atténuées des annonces consultées ; - utilitaires : `bboxOfProperties`, `pointInPolygon`, `bboxToString`, `cameraToParams`/`cameraFromParams`. **L'app** possède : l'état de recherche, la requête unifiée, l'URL, la liste, le carrousel mobile et le langage visuel des cartes/mini-fiches. ## La machine d'états côté app (copier ce comportement) 1. **Arrivée sans caméra dans l'URL** : requête sans zone → `fitBounds` animé sur les résultats (`byUser: false`, ne verrouille pas la vue). 2. **Arrivée avec caméra (lien partagé)** : la zone visible restaurée devient la contrainte spatiale de la première requête ; vue considérée verrouillée. 3. **Geste utilisateur** (`byUser: true`) : verrouille la vue ; si « Rechercher quand je déplace la carte » (défaut : coché) → nouvelle requête avec la bbox (debounce ~250 ms après la fin du geste) ; sinon marquer la zone divergée et montrer « Rechercher dans cette zone ». 4. **Filtre modifié** : page 1 ; si la vue n'est PAS verrouillée → requête sans zone + fitBounds ; si verrouillée → requête dans la zone courante et bouton discret « Recadrer sur les résultats ». 5. **Polygone dessiné** : remplace la bbox (il EST la zone), devient une puce retirable, encodé dans l'URL (`zone=lng,lat;…`), fitBounds sur son contenu. 6. **Tri/page** : même requête ; un changement de page passe `include=liste` (les points, identiques, ne sont pas retéléchargés). 7. **URL** : caméra (`lat/lng/zoom`) + `tri` + `page` + `zone` + `move=0` via `replaceState` — partager le lien reproduit la recherche à l'identique. ## Miroirs (latence < 100 ms) - survol carte d'annonce → `setHovered(uid, "app")` (feature-state, aucun re-rendu carte) ; - survol marqueur → événement `hover` → classe CSS sur la carte d'annonce + indicateur « ▲/▼ annonce hors écran » si elle n'est pas visible ; - clic marqueur → `select` → page ajustée au besoin → défilement animé + pulsation ; - clic annonce → `select(uid, "app")` → recentrage doux (uniquement si hors champ) + mini-fiche ; Cmd/Ctrl-clic ou 2ᵉ clic → navigation vers la fiche. ## Mobile Bascule Liste ↔ Carte sans perte (état dans l'URL). En vue carte : carrousel horizontal `scroll-snap` en bas, alimenté par la MÊME page de liste ; balayage → sélection du marqueur (`select`, origin `"app"`), marqueur tapé → défilement du carrousel. Un seul état, deux projections. ## Anti-régression - E2E : `lou-ka/scripts/test-sync.mjs` (25 vérifications, critères 1-9). - Unitaires framework : `ka-maps/tests/drawGeo.test.ts` (+ suites existantes). - API : `lou-ka/tests/test_search.py` (règle d'or, tris, polygone, bornes).