france-data-mcp 0.12.3 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -79,14 +79,16 @@ Brings together the most useful French government data sources under a uniform t
79
79
 
80
80
  ## Status
81
81
 
82
- ✅ **v0.12.3 — in production.** 35 tools, 4 ingested health sources (FINESS, Ameli, RPPS, CDS) + live INSEE / DINUM / IGN. 1,206 tests, TypeScript strict. Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observability: Sentry + Axiom + `/healthz`. **68.5% of RPPS practitioners precisely geolocated** (FINESS building or BAN street) — exposed via per-result `geo_precision` + `precise_only` filter on 4 RPPS tools.
82
+ ✅ **v0.13.1 — in production.** 35 tools, 1,263 tests, strict TypeScript. Listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observability: Sentry + Axiom + `/healthz`. 68.5% of RPPS practitioners precisely geolocated (`precise_only` filter exposed on 4 tools).
83
83
 
84
- Full history: [CHANGELOG](CHANGELOG.md). Project discipline: prove every root cause against production / official docs BEFORE coding the fix.
84
+ Feature-by-version details: [CHANGELOG](CHANGELOG.md). Project discipline: prove every root cause against production BEFORE coding the fix.
85
85
 
86
86
  ### Roadmap
87
87
 
88
- - [ ] **Phase 2 — recurring re-geocoding automation**: bounded BAN step inline in the RPPS cron (diff staging POST BAN on delta upsert cache). Pending Phase 1 production numbers.
89
- - [ ] **v1.0+**: DOM-COM support (`code_insee CHAR(5)`), INSEE IRIS (infra-communal demographics), DPC (PS training history).
88
+ - [ ] **Dense-zone timeout** on `rpps_in_radius` `preciseOnly=true` (Paris 1 km) `EXPLAIN ANALYZE` + index/planner fix.
89
+ - [ ] **Ameli BAN geocoding** (commune centroid precise street).
90
+ - [ ] **Unified RPPS + Ameli PS sheet** (concatenated, divergences exposed to caller).
91
+ - [ ] **v1.0+**: DOM-COM, INSEE IRIS, DPC.
90
92
 
91
93
  ---
92
94
 
package/README.md CHANGED
@@ -112,14 +112,16 @@ Usage intensif : throttler côté client ou self-héberger.
112
112
 
113
113
  ## État du projet
114
114
 
115
- ✅ **V0.12.3 — en production.** 35 tools, 4 sources santé (FINESS, Ameli, RPPS, CDS) + INSEE / DINUM / IGN. 1 206 tests, TypeScript strict. Sur le [registry MCP officiel](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observabilité Sentry + Axiom + `/healthz`. **68,5 % des PS RPPS géolocalisés précisément** (FINESS bâtiment ou BAN rue) — exposé via `geo_precision` + filtre `precise_only` par-résultat sur 4 tools RPPS.
115
+ ✅ **V0.13.1 — en production.** 35 tools, 1 263 tests, TypeScript strict. Sur le [registry MCP officiel](https://registry.modelcontextprotocol.io/v0.1/servers?search=france-data-mcp). Observabilité Sentry + Axiom + `/healthz`. 68,5 % des PS RPPS géolocalisés précisément (filtre `precise_only` exposé sur 4 tools).
116
116
 
117
- Historique complet : [CHANGELOG](CHANGELOG.md). Discipline projet : prouver chaque cause-racine par la prod / doc officielle AVANT de coder le fix.
117
+ Détail des features par version : [CHANGELOG](CHANGELOG.md). Discipline projet : prouver chaque cause-racine par la prod AVANT de coder le fix.
118
118
 
119
119
  ### Roadmap
120
120
 
121
- - [ ] **Phase 2 — automatisation re-géocodage récurrent** : brique BAN inline cron RPPS (diff staging POST BAN delta upsert cache). Conditionnée aux chiffres Phase 1.
122
- - [ ] **V1.0+** : DOM-COM (`code_insee CHAR(5)`), INSEE IRIS (démographie infra-communale), DPC (historique formations PS).
121
+ - [ ] **Timeout zone dense** `rpps_in_radius` `preciseOnly=true` (Paris 1 km) `EXPLAIN ANALYZE` + fix indices / planner.
122
+ - [ ] **Géocodage Ameli BAN** (centroïde commune rue précise).
123
+ - [ ] **Fiche PS unifiée RPPS + Ameli** (concaténée, divergences exposées au caller).
124
+ - [ ] **V1.0+** : DOM-COM, INSEE IRIS, DPC.
123
125
 
124
126
  ---
125
127
 
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
7
  // src/core/version.ts
8
- var VERSION = "0.12.3";
8
+ var VERSION = "0.13.1";
9
9
 
10
10
  // bin/cli.ts
11
11
  var DEFAULT_ENDPOINT = "https://france-data-mcp.vercel.app/mcp";
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.12.3\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
1
+ {"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.13.1\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Commune, GeoLevel, GeocodeOptions, GeocodeResult, PopulationData, SearchCommunesOptions, TERRITOIRE_VERSION, geocode, geocodeMany, getCommuneByCode, getPopulationByCommune, getPopulationByDept, reverseGeocode, searchCommunes } from './territoire/index.js';
2
- export { AnsFhirPractitioner, ByCategorieInput, Dirigeant, Entreprise, Etablissement, EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, FilterAnnuaireOptions, Finance, FinessCategorieCode, FinessFamille, FinessFamilleQuery, FinessQueryResult, FinessResult, InRadiusInput, LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, NafCodeSante, ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, RppsDansEtablissementInput, RppsInRadiusInput, RppsLookupResult, RppsParSpecialiteDeptInput, RppsQueryResult, RppsResult, SANTE_VERSION, SearchEntreprisesOptions, SearchEntreprisesResult, SearchFinessOptions, StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, searchEntreprises, searchEtablissementsFiness, streamProfessionnels } from './sante/index.js';
2
+ export { AnsFhirPractitioner, ByCategorieInput, DELIBERATELY_NO_NAF, DinumLookupError, Dirigeant, DisambiguationStatus, Entreprise, Etablissement, EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, FallbackReason, FilterAnnuaireOptions, Finance, FinessCategorieCode, FinessFamille, FinessFamilleQuery, FinessQueryResult, FinessResult, InRadiusInput, LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, NafCodeSante, ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, ResolutionMethod, RppsDansEtablissementInput, RppsInRadiusInput, RppsLookupResult, RppsParSpecialiteDeptInput, RppsQueryResult, RppsResult, SANTE_VERSION, SearchEntreprisesOptions, SearchEntreprisesResult, SearchFinessOptions, SiretCandidate, SiretCandidateSource, SiretResolution, StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, isNafCompatibleWithFamille, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, nafsForFamille, searchEntreprises, searchEtablissementsFiness, streamProfessionnels } from './sante/index.js';
3
3
  export { C as Coordinates, R as RateLimitOptions } from './types-6cvLQmuz.js';
package/dist/index.js CHANGED
@@ -1401,6 +1401,70 @@ function libelleNaf(code) {
1401
1401
  return NAF_SANTE[code];
1402
1402
  }
1403
1403
 
1404
+ // src/sante/naf-finess-mapping.ts
1405
+ var NAF_BY_FAMILLE = {
1406
+ // ─── Sanitaire ────────────────────────────────────────────────────────
1407
+ mco: ["8610Z"],
1408
+ ssr: ["8610Z"],
1409
+ sld: ["8610Z"],
1410
+ had: ["8610Z"],
1411
+ psychiatrie: ["8610Z", "8720A"],
1412
+ dialyse: ["8610Z", "8690F"],
1413
+ ambulatoire: ["8621Z", "8622A", "8622B", "8622C", "8623Z", "8690F"],
1414
+ // ─── Bio / pharma / imagerie ──────────────────────────────────────────
1415
+ labo: ["8690B"],
1416
+ // Imagerie : 8622A pour les cabinets de radiologie libéraux (radiodiagnostic
1417
+ // et radiothérapie au sens INSEE), 8690F pour les centres montés en société
1418
+ // de moyens (SCM/SEL) classés dans le fourre-tout santé humaine n.c.a. —
1419
+ // sans ce filet, on raterait une part importante des centres d'imagerie en
1420
+ // fallback (ajustement Cyril 2026-05-21).
1421
+ imagerie: ["8622A", "8690F"],
1422
+ pharmacie: ["4773Z"],
1423
+ // ─── Pluri-pro ────────────────────────────────────────────────────────
1424
+ msp_cpts: ["8621Z", "8622C", "8690F"],
1425
+ // ─── Personnes âgées ──────────────────────────────────────────────────
1426
+ ehpad: ["8710A", "8730A"],
1427
+ residence_autonomie: ["8730A", "8710A"],
1428
+ senior_accompagnement: ["8810B"],
1429
+ // ─── Domicile ─────────────────────────────────────────────────────────
1430
+ ssiad: ["8690D", "8810A"],
1431
+ aide_domicile: ["8810A", "8690D"],
1432
+ // ─── Handicap ─────────────────────────────────────────────────────────
1433
+ handicap_enfants: ["8710B", "8891B", "8899A"],
1434
+ handicap_adultes: ["8710C", "8720A", "8730B", "8810B", "8810C"],
1435
+ // ─── Addictologie / précarité sanitaire ───────────────────────────────
1436
+ addictologie: ["8610Z", "8720B", "8690F"],
1437
+ // ─── Enfance / protection ─────────────────────────────────────────────
1438
+ enfance_protection: ["8891A", "8899A"],
1439
+ // ─── PMI / petite enfance / santé scolaire ────────────────────────────
1440
+ pmi: ["8621Z", "8690F"],
1441
+ // ─── Hébergement social ───────────────────────────────────────────────
1442
+ hebergement_social: ["8720A", "8720B", "8730A", "8730B", "8899B"],
1443
+ // ─── Prévention / santé publique ──────────────────────────────────────
1444
+ prevention_sante: ["8610Z", "8690F"],
1445
+ // ─── Groupements de coopération ───────────────────────────────────────
1446
+ // ⚠️ AUCUN NAF : un GCS/GCSMS est une structure juridique transverse, pas
1447
+ // une activité économique propre. Listé dans DELIBERATELY_NO_NAF — le
1448
+ // fallback géo est désactivé pour cette famille (skip silencieux).
1449
+ // CI invariant `groupement is in DELIBERATELY_NO_NAF`.
1450
+ groupement: []
1451
+ };
1452
+ var DELIBERATELY_NO_NAF = /* @__PURE__ */ new Set([
1453
+ "groupement"
1454
+ ]);
1455
+ function nafsForFamille(famille) {
1456
+ if (famille === "autre") return [];
1457
+ if (DELIBERATELY_NO_NAF.has(famille)) return [];
1458
+ return NAF_BY_FAMILLE[famille] ?? [];
1459
+ }
1460
+ function isNafCompatibleWithFamille(naf, famille) {
1461
+ if (!naf) return false;
1462
+ const compatibles = nafsForFamille(famille);
1463
+ if (compatibles.length === 0) return false;
1464
+ const normalized = naf.replace(/\./g, "").toUpperCase().trim();
1465
+ return compatibles.includes(normalized);
1466
+ }
1467
+
1404
1468
  // src/core/query-metadata.ts
1405
1469
  var SOURCE_NOTE = {
1406
1470
  centroide_commune_ameli: "Coordonn\xE9es Ameli = centro\xEFde commune (~3 km moyenne). Adapt\xE9 \xE0 l'analyse de densit\xE9 m\xE9dicale, pas au g\xE9ocodage adresse.",
@@ -1408,21 +1472,29 @@ var SOURCE_NOTE = {
1408
1472
  /** @deprecated V0.12.0 — Plus aucune RPC RPPS ne produit cet alias ; toutes ont migré vers `centroide_commune_ans_mixte` (précision hybride par-résultat). Conservé pour rétrocompat de tout client qui aurait caché la string `geo_precision` côté query_metadata (Claude.ai, Cursor, agents loggant). Ne pas réintroduire dans un nouveau call site. */
1409
1473
  centroide_commune_ans: "Coordonn\xE9es RPPS/ANS = centro\xEFde commune (~3 km moyenne). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour une pr\xE9cision adresse, croiser num_finess avec etablissement_by_finess.",
1410
1474
  centroide_commune_ans_mixte: 'Coordonn\xE9es RPPS HYBRIDES (V0.12.0) : la pr\xE9cision est MIXTE par r\xE9sultat \u2014 lire `geo_precision` PAR PS. ~68,5 % sont pr\xE9cis (`"adresse"` BAN rue/b\xE2timent ou `"etablissement_finess"` site FINESS joint via num_finess) avec `distance_km` exacte ; ~31,5 % restent au centro\xEFde commune (`"centroide_commune"`, ~3 km, `distance_km` non discriminante intra-commune). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour FORCER 100 % de r\xE9sultats pr\xE9cis (rayons courts <3 km, classement individuel), passer `precise_only: true` c\xF4t\xE9 tool radius.',
1475
+ centroide_commune_ans_precis_uniquement: "Coordonn\xE9es RPPS \u2014 variante effective V0.13.0 : TOUS les r\xE9sultats retourn\xE9s sur cette requ\xEAte sont en pr\xE9cision exacte (`adresse` BAN ou `etablissement_finess`). `distance_km` est exacte au m\xE8tre pr\xE8s pour chaque PS retourn\xE9, classement individuel fiable. Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. NB : la donn\xE9e source reste hybride (mixte) \u2014 d'autres PS au centro\xEFde commune existent peut-\xEAtre dans la zone mais \xE9taient hors rayon ou filtr\xE9s par `precise_only: true`.",
1476
+ centroide_commune_ans_centroide_uniquement: "Coordonn\xE9es RPPS \u2014 variante effective V0.13.0 : TOUS les r\xE9sultats retourn\xE9s sur cette requ\xEAte sont au centro\xEFde commune (~3 km). `distance_km` n'est PAS discriminante intra-commune (tous les PS d'une m\xEAme commune ont la m\xEAme distance au centre du rayon). Source : Annuaire Sant\xE9 ANS \u2014 Licence Ouverte v2.0. Pour un classement fiable, pivoter via FINESS (etablissement_by_finess) ou \xE9largir radius_km au-del\xE0 de ~3 km pour capter aussi les PS pr\xE9cis.",
1411
1477
  structure_finess: "Liste rattach\xE9e \xE0 un FINESS site. Le mode_exercice r\xE9v\xE8le la nature du lien (lib\xE9ral / salari\xE9). Couverture RPPS quand le PS l'a d\xE9clar\xE9 ; salari\xE9s CH/CHU/cliniques bien couverts.",
1412
1478
  centroide_commune_cds: "Coordonn\xE9es CDS = centro\xEFde commune (~3 km moyenne) \u2014 pas de coords natives dans le CSV CNAM. Source : Annuaire sant\xE9 Ameli, Assurance Maladie (mention obligatoire L.1461-2 CSP). Pivot via etab_finess vers FINESS DREES pour pr\xE9cision adresse."
1413
1479
  };
1414
1480
  var HAVERSINE_NOTE = "Distance calcul\xE9e en vol d'oiseau (haversine PostGIS). Pour la distance routi\xE8re, croiser avec un service externe (OSRM, ORS).";
1415
1481
  var CENTROIDE_COMMUNE_RESOLUTION_KM = 3;
1416
- var CENTROID_PRECISIONS = /* @__PURE__ */ new Set([
1417
- "centroide_commune_ameli",
1418
- "centroide_commune_ans",
1419
- "centroide_commune_cds"
1420
- ]);
1482
+ var CENTROID_PRECISIONS = {
1483
+ lambert93_natif_finess: false,
1484
+ centroide_commune_ameli: true,
1485
+ centroide_commune_ans: true,
1486
+ // deprecated, plus produit (cf. SOURCE_NOTE)
1487
+ centroide_commune_ans_mixte: false,
1488
+ centroide_commune_ans_precis_uniquement: false,
1489
+ centroide_commune_ans_centroide_uniquement: true,
1490
+ centroide_commune_cds: true,
1491
+ structure_finess: false
1492
+ };
1421
1493
  function isShortRadius(radiusKm) {
1422
1494
  return radiusKm !== void 0 && radiusKm < CENTROIDE_COMMUNE_RESOLUTION_KM;
1423
1495
  }
1424
1496
  function isSubCommuneRadius(precision, radiusKm) {
1425
- return isShortRadius(radiusKm) && CENTROID_PRECISIONS.has(precision);
1497
+ return isShortRadius(radiusKm) && CENTROID_PRECISIONS[precision];
1426
1498
  }
1427
1499
  var subCommuneRadiusNote = (radiusKm) => `radius_km=${radiusKm} < ${CENTROIDE_COMMUNE_RESOLUTION_KM} km : incompatible avec une pr\xE9cision au centro\xEFde commune. Le filtre rayon s'applique au centro\xEFde unique de chaque commune, pas aux adresses r\xE9elles \u2014 TOUS les PS d'une commune sont inclus ou exclus en bloc, et \`distance_km\` ne discrimine pas les PS d'une m\xEAme commune. Un r\xE9sultat vide peut \xEAtre un FAUX n\xE9gatif (centro\xEFde hors rayon), pas un d\xE9sert m\xE9dical. Pour une vraie g\xE9olocalisation adresse, pivoter via FINESS (etablissement_by_finess) ou \xE9largir radius_km \u2265 ${CENTROIDE_COMMUNE_RESOLUTION_KM}.`;
1428
1500
  function buildMetadata(precision, withDistance, radiusKm) {
@@ -1439,6 +1511,40 @@ function buildMetadata(precision, withDistance, radiusKm) {
1439
1511
  }
1440
1512
  var finessRadiusMetadata = () => buildMetadata("lambert93_natif_finess", true);
1441
1513
  var finessByCategorieMetadata = () => buildMetadata("lambert93_natif_finess", false);
1514
+ function refineRppsGeoPrecisionLabel(rows, baseMeta) {
1515
+ if (baseMeta.geo_precision !== "centroide_commune_ans_mixte") return baseMeta;
1516
+ if (rows.length === 0) return baseMeta;
1517
+ let precisCount = 0;
1518
+ let centroideCount = 0;
1519
+ for (const row of rows) {
1520
+ const p = row.geo_precision;
1521
+ if (p === "adresse" || p === "etablissement_finess") precisCount++;
1522
+ else if (p === "centroide_commune") centroideCount++;
1523
+ else {
1524
+ console.warn(
1525
+ `[france-data-mcp] refineRppsGeoPrecisionLabel: row sans geo_precision typ\xE9 (valeur=${JSON.stringify(p)}) \u2014 \xE9tiquette mixte pr\xE9serv\xE9e par s\xE9curit\xE9, drift RPC suspect\xE9e si coords non-null.`
1526
+ );
1527
+ return baseMeta;
1528
+ }
1529
+ }
1530
+ let refinedPrecision;
1531
+ if (precisCount === rows.length) {
1532
+ refinedPrecision = "centroide_commune_ans_precis_uniquement";
1533
+ } else if (centroideCount === rows.length) {
1534
+ refinedPrecision = "centroide_commune_ans_centroide_uniquement";
1535
+ } else {
1536
+ return baseMeta;
1537
+ }
1538
+ let trailingNotes = baseMeta.notes.slice(1);
1539
+ if (refinedPrecision === "centroide_commune_ans_centroide_uniquement") {
1540
+ trailingNotes = trailingNotes.filter((n) => !n.includes("La branche pr\xE9cise"));
1541
+ }
1542
+ return {
1543
+ ...baseMeta,
1544
+ geo_precision: refinedPrecision,
1545
+ notes: [SOURCE_NOTE[refinedPrecision], ...trailingNotes]
1546
+ };
1547
+ }
1442
1548
  var rppsRadiusMetadata = (radiusKm) => {
1443
1549
  const md = buildMetadata("centroide_commune_ans_mixte", true, radiusKm);
1444
1550
  if (isShortRadius(radiusKm)) {
@@ -1727,6 +1833,9 @@ async function getRppsInRadius(input) {
1727
1833
  limit,
1728
1834
  rppsRadiusMetadata(input.radiusKm)
1729
1835
  );
1836
+ if (result.query_metadata) {
1837
+ result.query_metadata = refineRppsGeoPrecisionLabel(result.results, result.query_metadata);
1838
+ }
1730
1839
  if (input.preciseOnly === true && result.count === 0 && result.query_metadata) {
1731
1840
  result.query_metadata.notes.push(
1732
1841
  "precise_only=true et 0 r\xE9sultat dans le rayon : il peut exister des PS au centro\xEFde commune dans la zone (geom_source='commune_centroid' exclus de la branche pr\xE9cise). Relancer avec precise_only=false (mode hybride) pour les inclure, ou \xE9largir radius_km."
@@ -2003,6 +2112,6 @@ function mapPractitioner(resource, expectedRpps) {
2003
2112
  // src/sante/index.ts
2004
2113
  var SANTE_VERSION = "0.5.1";
2005
2114
 
2006
- export { FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, SANTE_VERSION, TERRITOIRE_VERSION, ensureAnnuaireAmeli, finessFamille, geocode, geocodeMany, getAnsFhirApiKey, getAnsFhirBaseUrl, getCommuneByCode, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getPopulationByCommune, getPopulationByDept, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, reverseGeocode, searchCommunes, searchEntreprises, searchEtablissementsFiness, streamProfessionnels };
2115
+ export { DELIBERATELY_NO_NAF, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, SANTE_VERSION, TERRITOIRE_VERSION, ensureAnnuaireAmeli, finessFamille, geocode, geocodeMany, getAnsFhirApiKey, getAnsFhirBaseUrl, getCommuneByCode, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getPopulationByCommune, getPopulationByDept, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, isNafCompatibleWithFamille, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, nafsForFamille, reverseGeocode, searchCommunes, searchEntreprises, searchEtablissementsFiness, streamProfessionnels };
2007
2116
  //# sourceMappingURL=index.js.map
2008
2117
  //# sourceMappingURL=index.js.map