{"libraries":[{"name":"trace-smoother","title":"Trace smoother BRouter","category":"cartography","status":"stable","description":"Map-match des polylines sparse (Strava mapPolyline, GPX,\nOverland pings) sur le réseau OSM via BRouter, avec fidelity\nchecks contre une polyline dense de reference. Détecte les\ncoupures GPS réelles (voiture/train/bateau) et split en\nsous-tracés honnêtes (pas de fausse ligne routée entre).\n","keywords":["polyline","gpx","strava","overland","brouter","trace","map-matching","route","smoothing","mapPolyline","detailPolyline","osm","leaflet","cartography"],"use_when":["Une polyline Strava ressemble à une nuée de points sur la carte","Tracer un parcours suivant les routes OSM réelles","Smooth des pings GPS sparse (Overland 1/min) en tracé dense","Détecter les coupures GPS réelles dans un GPX (voiture/train)"],"avoid_when":["Tu veux la ground truth GPS exacte (utiliser detailPolyline brut)","Pas d'instance BRouter accessible (l'app fallback gracefully mais sans gain)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/trace-smoother","local_paths":["~/.claude/skills/atelier-libs/packages/trace-smoother","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/trace-smoother"],"install":{"degit":"npx degit ateliersam86/atelier-libs/packages/trace-smoother lib/trace-smoother\n","copy":"cp -r ~/.claude/skills/atelier-libs/packages/trace-smoother $PROJECT_ROOT/lib/\n"},"requirements":[{"id":"brouter","title":"BRouter instance HTTP","env_var":"BROUTER_URL","default":"http://192.168.1.72:17777/brouter","check":"curl -sf \"$BROUTER_URL/profile/trekking\" -o /dev/null","install":"# Self-hosted Docker (recommandé pour les projets Atelier)\ndocker run -d --name brouter -p 17777:17777 \\\n  -v ~/brouter/segments:/brouter/segments4 \\\n  -v ~/brouter/profiles2:/brouter/profiles2 \\\n  abrensch/brouter\n","public_fallback":"https://brouter.de/brouter"},{"id":"polyline_npm","title":"\"@mapbox/polyline\" pour encode/decode","check":"grep -q \"@mapbox/polyline\" package.json","install":"npm install @mapbox/polyline"}],"usage_example":"import { smoothWithCache, createDiskCache } from \"@/lib/trace-smoother\";\nimport polyline from \"@mapbox/polyline\";\n\nconst sparsePoints = polyline.decode(stravaMapPolyline)\n  .map(([lat, lon]) => ({ lat, lon }));\nconst referenceDense = polyline.decode(stravaDetailPolyline)\n  .map(([lat, lon]) => ({ lat, lon }));\n\nconst cache = createDiskCache({\n  rootDir: \"./cache/traces\",\n  namespace: \"trip-abc\",\n  id: \"segment-42\",\n});\n\nconst result = await smoothWithCache(sparsePoints, {\n  brouterUrl: process.env.BROUTER_URL ?? \"http://localhost:17777/brouter\",\n  profile: \"trekking\",        // ou fastbike, hiking-mountain\n  reference: referenceDense,  // ground truth pour fidelity\n  cache,\n});\n\n// result.subTraces: array de { points, source: \"brouter\"|\"raw\" }\n// result.diagnostics: { gapsDetected, sourceKm, smoothKm, rejects }\n","api_surface":["smoothTrace(sparse, opts) — algo pur, pas de cache","smoothWithCache(sparse, opts & { cache }) — convenience wrapper","createDiskCache({ rootDir, namespace, id }) — cache disk simple","callBrouter(waypoints, opts) — bas niveau","splitByGaps, haversineKm, polylineKm — geo primitives"],"docs_url":"./trace-smoother/README.md","added":"2026-05-12","last_validated":"2026-05-12"},{"name":"use-live-data","title":"Hook React partagé pour /live polling","category":"live","status":"stable","description":"Hook TanStack Query qui mutualise le polling /live entre N\nconsumers React. Résout le problème : 14 composants qui poll\nindépendamment le même endpoint saturent la queue HTTP/1.1 du\nbrowser (max 6 connexions) et stallent les chunks critiques.\n1 fetch partagé, tout le monde lit depuis le cache TanStack.\n","keywords":["live","polling","tanstack","react-query","hook","shared","dedup","tracking","sse-alternative","overland"],"use_when":["Plusieurs composants React lisent le même endpoint /live ou similaire","Tu veux mutualiser un polling périodique entre N consumers","Le browser stall sur 6+ requêtes concurrentes"],"avoid_when":["Le polling est unique (1 seul consumer)","Tu as besoin de WebSocket / SSE (utiliser EventSource direct)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/useLiveData.ts","local_paths":["~/.claude/skills/atelier-libs/packages/useLiveData.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/useLiveData.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/useLiveData.ts $PROJECT_ROOT/lib/\n","degit":"# 1 fichier — copie manuelle ou via curl\ncurl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/useLiveData.ts \\\n  -o lib/useLiveData.ts\n"},"requirements":[{"id":"tanstack_query","title":"@tanstack/react-query installé","check":"grep -q \"@tanstack/react-query\" package.json","install":"npm install @tanstack/react-query"},{"id":"query_provider","title":"QueryClientProvider à la racine de l'app","check":"grep -rq \"QueryClientProvider\" app/ src/ 2>/dev/null","install":"# À ajouter dans ton app root (Next.js : app/providers.tsx)\n# Pattern standard TanStack Query setup\n"}],"usage_example":"import { useLiveData } from \"@/lib/useLiveData\";\n\nfunction MyWidget({ slug }) {\n  const liveQuery = useLiveData(slug, { enabled: true });\n  const data = liveQuery.data ?? null;\n  return <div>{data?.last?.timestamp}</div>;\n}\n\n// 14 instances de MyWidget = 1 seul fetch /live partagé.\n","api_surface":["useLiveData(slug, { enabled? }) → UseQueryResult<LiveData>","type LiveData (flexible, key/value)"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"use-geocode-reverse","title":"Hook React partagé pour reverse-geocode cached","category":"live","status":"stable","description":"Hook TanStack Query qui partage les fetches /api/geocode/reverse\nentre N composants. Cache keyé sur (lat, lon) arrondis à 5 décimales\n(~1m précision), staleTime: Infinity (les labels OSM ne changent\njamais). Résout le problème : 84 fetches geocode au load → 14\nuniques après dédup → queue HTTP libérée.\n","keywords":["geocode","geocoding","reverse-geocode","nominatim","tanstack","react-query","hook","shared","dedup","location","label"],"use_when":["Plusieurs composants affichent le label d'une même coord (start/end de jour, etc.)","Tu fais 30+ fetches /geocode/reverse au load d'une page","Le browser stall sur les requêtes parallèles"],"avoid_when":["Un seul reverse-geocode dans toute la page (overkill)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/useGeocodeReverse.ts","local_paths":["~/.claude/skills/atelier-libs/packages/useGeocodeReverse.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/useGeocodeReverse.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/useGeocodeReverse.ts $PROJECT_ROOT/lib/\n","degit":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/useGeocodeReverse.ts \\\n  -o lib/useGeocodeReverse.ts\n"},"requirements":[{"id":"tanstack_query","title":"@tanstack/react-query installé","check":"grep -q \"@tanstack/react-query\" package.json","install":"npm install @tanstack/react-query"},{"id":"api_endpoint","title":"Endpoint /api/geocode/reverse côté serveur (Nominatim wrapper avec cache)","check":"ls app/api/geocode/reverse/route.ts 2>/dev/null","install":"# À implémenter selon ton stack. Le hook attend la shape :\n# { label: string | null }\n"}],"usage_example":"import { useGeocodeReverse } from \"@/lib/useGeocodeReverse\";\n\nfunction DayMarker({ coord }) {\n  const label = useGeocodeReverse(coord.lat, coord.lon);\n  return <span>{label ?? \"Chargement...\"}</span>;\n}\n","api_surface":["useGeocodeReverse(lat, lon) → string | undefined"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"auto-login","title":"Auto-login pipeline (HumanCursor + Claude Vision)","category":"scraping","status":"stable","description":"Pipeline autonome pour login sur sites e-commerce avec reCAPTCHA v2,\nCloudflare Turnstile, ou formulaires complexes. Patchright stealth +\nmouvements souris Bezier humanisés (pyclick.HumanCurve) + résolution\nimage grid via Claude Vision (Meridian wrapper / Anthropic API /\ncompatible OpenAI). State.json cookies persistés <30j. Recordings webm\n+ screenshots à chaque tentative pour debug post-mortem. Fallback\ngracieux : si LLM down, flag UI Login VNC manuel. Zéro service tiers\npayant si OAuth Claude Code Max dispo.\n","keywords":["captcha","recaptcha","cloudflare","turnstile","scraping","login","patchright","playwright","stealth","humancursor","bezier","claude-vision","prestashop","magento","fournisseur","cookies","state.json","auto-login","anti-bot"],"use_when":["Tu scrapes un fournisseur B2B (PrestaShop / Magento / custom) avec reCAPTCHA","Tu veux automatiser le login + résolution captcha sans 2captcha payant","Cookies session expirent régulièrement et tu veux auto-refresh","Sites Cloudflare bloquent Playwright vanilla → besoin Patchright stealth","Tu veux des recordings post-mortem pour debug quand un scraper fail"],"avoid_when":["Pas de wrapper Claude Code OAuth ni clé Anthropic dispo → fallback VNC manuel","Site sans captcha — httpx + cookies basiques suffisent","reCAPTCHA Enterprise v3 score-only (pas d'interaction visible)"],"repo":"github.com/ateliersam86/hub-atelier-de-sam","repo_subpath":"backend/scrapers/captcha","local_paths":["/Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/captcha","/Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/auto_login_pipeline.py"],"install":{"copy":"cp -r /Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/captcha $PROJECT_ROOT/lib/\ncp /Users/samuelmuselet/A-Projets/atelier/hub-atelier-de-sam/backend/scrapers/auto_login_pipeline.py $PROJECT_ROOT/lib/captcha/\n"},"requirements":[{"id":"patchright","title":"Patchright (Playwright fork stealth)","check":"python3 -c 'import patchright'","install":"pip install patchright>=1.49.0 && patchright install chrome"},{"id":"pyclick","title":"pyclick (HumanCurve Bezier trajectories)","check":"python3 -c 'from pyclick.humancurve import HumanCurve'","install":"pip install pyclick==0.0.2 && apt install python3-tk"},{"id":"pillow","title":"PIL (image downscale)","check":"python3 -c 'from PIL import Image'","install":"pip install Pillow"},{"id":"vision-llm","title":"Wrapper Claude/GPT vision (au choix)","env_var":"MERIDIAN_WRAPPERS","default":"http://192.168.1.72:3459|wrapper-op-meridian-2","check":"curl -sf \"${MERIDIAN_WRAPPERS%%,*}\" | grep -q healthy"},{"id":"xvfb","title":"Xvfb display server (pour Patchright headful en container)","check":"which Xvfb","install":"apt install xvfb"}],"usage_example":"from scrapers.captcha import HumanMouse, solve_image_grid\nfrom scrapers.auto_login_pipeline import auto_login_async\n\n# Cas 1: pipeline complet (httpx scraper)\ncookies = await auto_login_async(\n    slug=\"brico-phone\",\n    login_url=\"https://www.brico-phone.com/connexion\",\n    email=\"…\", password=\"…\",\n    target_domain=\"brico-phone.com\",\n)\nif cookies:\n    for c in cookies:\n        httpx_client.cookies.set(c[\"name\"], c[\"value\"], domain=c[\"domain\"])\n\n# Cas 2: solver vision seul (pour reCAPTCHA dans ton propre flow)\ncells = await solve_image_grid(\n    screenshot_path=\"/tmp/recaptcha.png\",\n    instruction=\"Sélectionnez toutes les images montrant des vélos\",\n    grid_size=3,\n)\n# cells = [1, 5, 7] → click ces cells dans l'iframe bframe\n\n# Cas 3: HumanCursor seul (humaniser n'importe quel click Playwright)\nmouse = HumanMouse(page)\nawait mouse.click_at(450, 320)  # trajectoire Bezier + jitter timing\n","api_surface":["auto_login_async(slug, login_url, email, pwd, target_domain) -> list[cookie] | None","recover_session_for_playwright(page, slug, login_url, …) -> bool","solve_image_grid(screenshot_path, instruction, grid_size=3) -> list[int] | None","HumanMouse(page).click_at(x, y) / click_locator(loc) / type_into(sel, val)","load_fresh_state_cookies(slug, max_age_days=30) -> list[cookie] | None","save_state_cookies(slug, cookies)"],"notes":"- Recordings webm dans data/auto_login_recordings/<slug>/<timestamp>/\n- Vision provider configurable via MERIDIAN_WRAPPERS env\n  (format: \"url1|api_key1,url2|api_key2\", premier qui répond 200 wins)\n- Testé live le 2026-05-12 : Meridian-2 (port 3459, Claude Haiku 4.5)\n  résout reCAPTCHA \"feux de circulation\" en ~5s, cells [1,3,7,8] OK\n- Câblé dans 4 scrapers Hub : invoice_prestashop, foneday, mobilax,\n  jensmobiles (commits 4848579 → cb70013)\n- Lib pas encore extraite en repo dédié, vit dans le mono-repo Hub.\n  Extract en humanlogin/pip si traction (~1 jour de boulot).\n","added":"2026-05-12","last_validated":"2026-05-12"},{"name":"client-admin-auth","title":"Admin password client-side (sessionStorage)","category":"auth","status":"stable","description":"Helpers React pour gérer un mot de passe admin côté client :\nverify contre un endpoint serveur, stocker en sessionStorage\n(jamais en localStorage, jamais hardcodé dans le bundle),\nrelire pour les appels admin suivants. Namespace par scope\n(default + linked-trip keys) — plusieurs admin coexistent sur\nle même tab. Pattern utilisé sur tous les admin UI Atelier.\n","keywords":["admin","auth","password","sessionStorage","login","gate","middleware-client","react"],"use_when":["Créer un admin UI où l'utilisateur tape un mot de passe","Plusieurs admin scopes coexistent (Sam admin, Paul admin, etc.)","Tu veux un pattern simple sans Auth0/Clerk pour un usage perso"],"avoid_when":["Tu as besoin d'un vrai système d'auth multi-utilisateur (utiliser Clerk/Auth0)","Côté serveur seul (utiliser lib/adminAuth.ts du même projet)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/clientAdminAuth.ts","local_paths":["~/.claude/skills/atelier-libs/packages/clientAdminAuth.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/lib/clientAdminAuth.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/clientAdminAuth.ts $PROJECT_ROOT/lib/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/clientAdminAuth.ts \\\n  -o lib/clientAdminAuth.ts\n"},"requirements":[{"id":"verify_endpoint","title":"Endpoint serveur POST /api/admin/verify-password","check":"ls app/api/admin/verify-password/route.ts 2>/dev/null","install":"# Wrapper côté serveur qui valide le password contre une env var\n# (timing-safe comparaison + rate-limit). Voir lib/adminAuth.ts\n# dans atelier-web-travels pour la référence d'implémentation.\n"}],"usage_example":"\"use client\";\nimport { verifyAdminPasswordAndStore, getStoredAdminPassword } from \"@/lib/clientAdminAuth\";\n\n// Gate component\nasync function handleSubmit(password: string) {\n  const { ok } = await verifyAdminPasswordAndStore(password);\n  if (ok) router.push(\"/admin/dashboard\");\n}\n\n// Subsequent admin API calls\nconst adminPassword = getStoredAdminPassword();\nawait fetch(\"/api/admin/trips/x\", {\n  headers: { \"x-admin-password\": adminPassword },\n});\n\n// Linked-trip scope (multi-admin)\nawait verifyAdminPasswordAndStore(pwd, \"paul\");\nconst paulPwd = getStoredAdminPassword(\"paul\");\n","api_surface":["verifyAdminPasswordAndStore(candidate, linkedTripKey?) → { ok, status }","getStoredAdminPassword(linkedTripKey?) → string","setStoredAdminPassword(password, linkedTripKey?)","clearStoredAdminPassword(linkedTripKey?)"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"leaflet-loader","title":"Dynamic Leaflet import + React hook","category":"cartography","status":"stable","description":"Wrapper léger pour charger Leaflet de façon dynamique (jamais\nimporté pendant SSR). Mémoise la Promise — N composants qui\nutilisent la map partagent le même import. Hook `useLeaflet()`\npour les Components React. Empêche les \"ReferenceError: window\nis not defined\" et les double-imports qui font stall les chunks\nLeaflet 20s dans la queue HTTP du browser.\n","keywords":["leaflet","react","dynamic-import","ssr","hook","map","cartography","bundle-split"],"use_when":["App Next.js/SSR avec une map Leaflet","Plusieurs composants utilisent Leaflet et tu veux mutualiser le chargement","Tu veux éviter `import 'leaflet'` direct qui plante en SSR"],"avoid_when":["Pas de SSR (tu peux import direct)","Tu utilises Mapbox/MapLibre (incompatible)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/leafletLoader.ts","local_paths":["~/.claude/skills/atelier-libs/packages/leafletLoader.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/utils/leafletLoader.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/leafletLoader.ts $PROJECT_ROOT/utils/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/leafletLoader.ts \\\n  -o utils/leafletLoader.ts\n"},"requirements":[{"id":"leaflet_npm","title":"leaflet installé","check":"grep -q \"\\\"leaflet\\\"\" package.json","install":"npm install leaflet && npm install --save-dev @types/leaflet"}],"usage_example":"\"use client\";\nimport { useLeaflet, loadLeaflet } from \"@/utils/leafletLoader\";\n\n// Hook React (Component pattern)\nfunction MapComponent() {\n  const leaflet = useLeaflet();\n  if (!leaflet) return <div>Loading map...</div>;\n  // use leaflet.map(), leaflet.tileLayer(), etc.\n}\n\n// Promise pattern (useEffect direct)\nuseEffect(() => {\n  let map;\n  loadLeaflet().then((leaflet) => {\n    map = leaflet.map(ref.current).setView([0, 0], 2);\n  });\n  return () => map?.remove();\n}, []);\n","api_surface":["loadLeaflet() → Promise<LeafletModule>","useLeaflet() → LeafletModule | null","type LeafletModule"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"shorten-place-label","title":"Verbose place name → 'City · Country'","category":"cartography","status":"stable","description":"Tronque les labels géocodés verbeux (\"Nice, Provence-Alpes-Côte\nd'Azur, France\") en un format compact (\"Nice · France\") via\nmapping REGION_TO_COUNTRY. Garde les régions courtes telles\nquelles (\"Alès · France\"). Pratique pour les tooltip de carte,\npilules live, badges. Couvre toutes les régions de France\nmétropolitaine + DOM-TOM, extensible par dictionnaire.\n","keywords":["geocoding","label","format","nominatim","tooltip","shorten","cartography"],"use_when":["Tu affiches des labels de lieu sur une carte/pilule (espace contraint)","Nominatim te renvoie 3 parts mais tu en veux 2"],"avoid_when":["Tu as besoin du label complet (sous-titre détail page, par ex.)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/shortenPlaceLabel.ts","local_paths":["~/.claude/skills/atelier-libs/packages/shortenPlaceLabel.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/utils/shortenPlaceLabel.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/shortenPlaceLabel.ts $PROJECT_ROOT/utils/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/shortenPlaceLabel.ts \\\n  -o utils/shortenPlaceLabel.ts\n"},"usage_example":"import { shortenPlaceLabel } from \"@/utils/shortenPlaceLabel\";\n\nshortenPlaceLabel(\"Nice, Provence-Alpes-Côte d'Azur, France\");\n// → \"Nice · France\"\n\nshortenPlaceLabel(\"Alès, Occitanie\");\n// → \"Alès · France\" (via REGION_TO_COUNTRY)\n\nshortenPlaceLabel(\"Paris\");\n// → \"Paris\"\n","api_surface":["shortenPlaceLabel(raw: string) → string"],"added":"2026-05-12","last_validated":"2026-05-12"},{"name":"poi-lookup","title":"POI nearest via Overpass OSM + cache 24h","category":"cartography","status":"stable","description":"Cherche le POI nommé le plus pertinent dans un rayon autour\nd'une coordonnée GPS (restaurant, café, hôtel, musée, château,\néglise, plage, gare, etc. — 60+ kinds). Source : Overpass API\n(OpenStreetMap, free, no key). Cache mem 24h + disk forever\npar (lat5, lon5, radius). Scoring kind-aware : un restaurant\ngagne contre un shop générique à distance égale. Tags étendus\n(cuisine, religion, museum:type, brand, country) pour des\nlibellés narratifs riches. STRONG_POI_KINDS exposé pour les\nheuristiques métier.\n","keywords":["poi","osm","openstreetmap","overpass","geocoding","place","nearest","cache","server-side","restaurant","cafe","museum","landmark"],"use_when":["Tu veux nommer le POI le plus proche d'une coord (live tracking, photo geotag, etc.)","Géo-narration : 'à 50m de la Cathédrale Saint-Front'","Détection de stops dans un GPX (couple avec trace-smoother)"],"avoid_when":["Tu n'as pas de runtime serveur (browser uniquement) — Overpass refuse CORS","Tu veux des POI hors-OSM (utiliser Foursquare/Google Places — paid)"],"repo":"github.com/ateliersam86/atelier-libs","repo_subpath":"packages/poiLookup.ts","local_paths":["~/.claude/skills/atelier-libs/packages/poiLookup.ts","/Users/samuelmuselet/A-Projets/atelier/atelier-web-travels/server/poiLookup.ts"],"install":{"copy":"cp ~/.claude/skills/atelier-libs/packages/poiLookup.ts $PROJECT_ROOT/server/\n","curl":"curl -fsSL https://raw.githubusercontent.com/ateliersam86/atelier-libs/main/packages/poiLookup.ts \\\n  -o server/poiLookup.ts\n"},"requirements":[{"id":"cache_dir","title":"Dossier de cache disk inscriptible","env_var":"POI_LOOKUP_CACHE_DIR","default":"./.cache/poi-lookup","check":"[ -w \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\" ] || mkdir -p \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\"","install":"mkdir -p \"${POI_LOOKUP_CACHE_DIR:-.cache/poi-lookup}\"\n"}],"usage_example":"import { findNearestPoi, STRONG_POI_KINDS } from \"@/server/poiLookup\";\n\n// Trouve le POI le plus pertinent dans 80m\nconst { poi, source } = await findNearestPoi(48.8566, 2.3522);\nif (poi) {\n  console.log(`${poi.name} (${poi.kind}, ${poi.distanceM}m)`);\n  if (poi.cuisine) console.log(`  cuisine: ${poi.cuisine}`);\n}\n// source: \"cache\" | \"disk\" | \"live\"\n\n// Test si un POI mérite un seuil stationary plus court\nif (poi && STRONG_POI_KINDS.has(poi.kind)) {\n  // user a probablement vraiment fait halte\n}\n","api_surface":["findNearestPoi(lat, lon, radius=80) → Promise<{ poi: PoiRecord | null, source }>","STRONG_POI_KINDS: ReadonlySet<PoiKind>","type PoiKind (60+ values), PoiRecord, PoiSource"],"added":"2026-05-12","last_validated":"2026-05-12"}],"generatedAt":"2026-05-12T03:53:54.097Z"}