france-data-mcp 0.7.2

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.
@@ -0,0 +1,1053 @@
1
+ import { C as Coordinates, L as LookupResult } from '../types-6cvLQmuz.js';
2
+
3
+ /**
4
+ * DINUM Recherche Entreprises — annuaire des entreprises françaises (SIRENE + RNE).
5
+ *
6
+ * URL : https://recherche-entreprises.api.gouv.fr/search
7
+ * Doc : https://recherche-entreprises.api.gouv.fr/docs/
8
+ *
9
+ * Rate limit documenté : 7 req/s. Observé en pratique : ~1 req/s effectif après le
10
+ * premier 429 (header retry-after: 4 systématique). Le helper fetchJson respecte
11
+ * retry-after, mais ne pas dépasser 1 req/s en burst pour éviter les 429 répétés.
12
+ *
13
+ * Sans clé API, sans authentification, CORS autorisé.
14
+ *
15
+ * Données utiles côté santé : le filtre `activite_principale` (NAF) permet de
16
+ * cibler labos (8690B), pharmacies (4773Z), maisons médicales (8621Z), SSR
17
+ * (8610Z), EHPAD (8710A), centres médico-sociaux, etc.
18
+ */
19
+
20
+ type Etablissement = {
21
+ /** SIRET 14 chiffres */
22
+ siret: string;
23
+ /** Adresse complète */
24
+ adresse: string;
25
+ /** Code postal */
26
+ codePostal?: string;
27
+ /** Commune */
28
+ commune?: string;
29
+ /** Coordonnées GPS si l'adresse est géocodée */
30
+ point?: Coordinates;
31
+ /** Code NAF de l'établissement */
32
+ naf?: string;
33
+ /** Établissement actif administrativement (etat_administratif === "A") */
34
+ actif: boolean;
35
+ /** Tranche d'effectif salarié (codes INSEE 0..53) */
36
+ trancheEffectif?: string;
37
+ /** Date de création de l'établissement */
38
+ dateCreation?: string;
39
+ };
40
+ type Finance = {
41
+ annee: number;
42
+ ca?: number;
43
+ resultatNet?: number;
44
+ /**
45
+ * Signal de fiabilité du `ca`. `false` quand `ca===0` ET `resultatNet>0` :
46
+ * pattern observé à 100% sur les SELARL pharma (NAF 47.73Z) qui ne déclarent
47
+ * pas leur CA au RNE — il ne faut pas l'afficher comme un vrai 0. Vraie
48
+ * dormance (`resultatNet<=0` ou undefined) reste `caFiable: true`.
49
+ */
50
+ caFiable: boolean;
51
+ };
52
+ type Dirigeant = {
53
+ nom?: string;
54
+ prenoms?: string;
55
+ fonction?: string;
56
+ qualite?: string;
57
+ };
58
+ /**
59
+ * État de l'enrichissement de la liste `etablissements` :
60
+ *
61
+ * - `not_attempted` : monosite (ou data SIRENE manquante) — pas de second appel.
62
+ * - `success` : `etablissements.length === nombreEtablissements` (ou >=).
63
+ * - `partial` : second appel OK mais retourne moins que le total SIRENE
64
+ * (cause typique : entreprise multi-département ou NAF secondaires).
65
+ * - `failed` : second appel a échoué (rate limit, panne API, parsing…).
66
+ * `enrichmentWarning` contient le message d'erreur.
67
+ */
68
+ type EnrichmentStatus = "not_attempted" | "success" | "partial" | "failed";
69
+ /** Source d'origine du lookup d'une `Entreprise`. Voir champ `siren_source` ci-dessous. */
70
+ type EntrepriseSirenSource = "dinum" | "insee_v3";
71
+ type Entreprise = {
72
+ /** SIREN 9 chiffres */
73
+ siren: string;
74
+ /** SIRET du siège (14 chiffres) */
75
+ siretSiege?: string;
76
+ /** Nom complet (raison sociale ou nom + prénom pour entrepreneurs individuels) */
77
+ nomComplet: string;
78
+ /** Code NAF principal */
79
+ naf?: string;
80
+ /** Libellé NAF principal */
81
+ nafLibelle?: string;
82
+ /** Tranche d'effectif (CA / RN dans `finances`) */
83
+ trancheEffectif?: string;
84
+ /** Code juridique INSEE */
85
+ natureJuridique?: string;
86
+ /** Finances historiques par année (les plus récentes sont en premier) */
87
+ finances: Finance[];
88
+ /** Dirigeants déclarés au RNE */
89
+ dirigeants: Dirigeant[];
90
+ /**
91
+ * Établissements actifs et inactifs.
92
+ *
93
+ * ⚠️ Pour `searchEntreprises({ q })` ou `getEntrepriseBySiren()`, ce champ peut
94
+ * ne contenir que le siège — l'API DINUM ne retourne que les établissements
95
+ * « matchant » la requête. `getEntrepriseBySiren()` fait un second appel
96
+ * automatique pour récupérer les établissements du même NAF principal dans
97
+ * le département du siège.
98
+ *
99
+ * **Le caller doit lire `enrichmentStatus`** pour savoir si la liste est
100
+ * complète (`success`), tronquée (`partial`), ou si l'enrichissement a échoué
101
+ * (`failed`). Comparer aussi `etablissements.length` à `nombreEtablissements`.
102
+ */
103
+ etablissements: Etablissement[];
104
+ /** Nombre total d'établissements (actifs + fermés), source SIRENE */
105
+ nombreEtablissements?: number;
106
+ /** Nombre d'établissements actuellement ouverts, source SIRENE */
107
+ nombreEtablissementsOuverts?: number;
108
+ /**
109
+ * État de l'enrichissement multi-sites (cf. `EnrichmentStatus`).
110
+ * Toujours présent pour les retours de `getEntrepriseBySiren()`.
111
+ * Absent pour les `searchEntreprises()` (pas d'enrichissement tenté).
112
+ */
113
+ enrichmentStatus?: EnrichmentStatus;
114
+ /** Message d'aide quand `enrichmentStatus` ∈ {"partial", "failed"}. */
115
+ enrichmentWarning?: string;
116
+ /**
117
+ * Source effective du lookup. `"dinum"` (défaut implicite) = retour de l'API
118
+ * recherche-entreprises.api.gouv.fr. `"insee_v3"` = fallback SIRENE INSEE V3
119
+ * activé quand DINUM ne connaît pas le SIREN (cas diffusion partielle). En
120
+ * mode insee_v3, finances/dirigeants/etablissements sont vides — l'API
121
+ * /siren/{siren} ne les expose pas sans appels supplémentaires /siret.
122
+ */
123
+ siren_source?: EntrepriseSirenSource;
124
+ /** Statut administratif global */
125
+ actif: boolean;
126
+ };
127
+ type SearchEntreprisesOptions = {
128
+ /** Recherche textuelle (raison sociale, dirigeant…) */
129
+ q?: string;
130
+ /** Filtre exact sur le code NAF (ex: "8690B" pour labos d'analyses médicales) */
131
+ naf?: string;
132
+ /** Filtre par code postal */
133
+ codePostal?: string;
134
+ /** Filtre par département (code 2 ou 3 caractères) */
135
+ departement?: string;
136
+ /** Filtre par code commune INSEE */
137
+ codeCommune?: string;
138
+ /**
139
+ * Recherche géographique : centre + rayon (km, max 50). DOIT être combiné
140
+ * avec `q` (recherche textuelle) — l'API DINUM rejette `naf + lat/lon/radius`
141
+ * directement. Pour combiner NAF + zone géographique, utiliser le tool MCP
142
+ * `entreprises_in_radius` qui applique un fallback automatique
143
+ * (reverseGeocode → département → filtre Haversine).
144
+ */
145
+ center?: Coordinates;
146
+ /** Rayon en km (1-50). Requis si `center` est fourni. */
147
+ radiusKm?: number;
148
+ /** Limiter aux établissements administrativement actifs (défaut: true) */
149
+ onlyActive?: boolean;
150
+ /** Page de résultats (1-indexed, défaut 1) */
151
+ page?: number;
152
+ /** Résultats par page (1-25, défaut 10) */
153
+ perPage?: number;
154
+ signal?: AbortSignal;
155
+ };
156
+ type SearchEntreprisesResult = {
157
+ total: number;
158
+ page: number;
159
+ perPage: number;
160
+ totalPages: number;
161
+ entreprises: Entreprise[];
162
+ };
163
+ /**
164
+ * Recherche d'entreprises avec filtres NAF / géo / texte libre.
165
+ *
166
+ * @example Tous les labos de bio médicale dans 5 km autour d'un point
167
+ * ```ts
168
+ * const labos = await searchEntreprises({
169
+ * naf: "8690B",
170
+ * center: { lon: 4.7192, lat: 49.7672 },
171
+ * radiusKm: 5,
172
+ * });
173
+ * ```
174
+ *
175
+ * @example Toutes les pharmacies du 08
176
+ * ```ts
177
+ * const pharma = await searchEntreprises({ naf: "4773Z", departement: "08" });
178
+ * ```
179
+ */
180
+ declare function searchEntreprises(options: SearchEntreprisesOptions): Promise<SearchEntreprisesResult>;
181
+ /**
182
+ * Récupère une entreprise par son SIREN (9 chiffres).
183
+ * Renvoie null si introuvable. Throw si l'API DINUM est en panne ou rate-limit dépassé.
184
+ *
185
+ * Implémentation :
186
+ * 1. `q=<siren>` (l'API DINUM matche le SIREN dans le full-text), filtrage côté
187
+ * client sur l'égalité exacte du SIREN.
188
+ * 2. **Limitation API DINUM** : `q=<siren>` ne retourne que le siège dans
189
+ * `matching_etablissements`. Pour récupérer les autres établissements, on
190
+ * fait un second appel `activite_principale=<naf>&departement=<dept_siège>`
191
+ * qui retourne tous les établissements de l'entreprise ayant le NAF
192
+ * principal dans le département du siège (couvre la majorité des
193
+ * multi-sites). Les `etablissements` du résultat fusionnent siège + ces
194
+ * établissements supplémentaires (déduplication par SIRET).
195
+ * 3. `nombreEtablissements` / `nombreEtablissementsOuverts` reflètent toujours
196
+ * le total réel SIRENE (non limité par l'API DINUM).
197
+ *
198
+ * **Limitation indexation DINUM** : certaines entreprises pourtant actives à
199
+ * l'INSEE/SIRENE ne sont PAS indexées par `recherche-entreprises.api.gouv.fr`
200
+ * (statut de diffusion partielle au sens INSEE — `statut_diffusion ∈ {P,N}` —
201
+ * ou exclusion sectorielle/légale). Ces SIREN reviennent `null` ici alors
202
+ * qu'ils existent réellement. L'audit post-v0.2.0 a vérifié ce comportement
203
+ * sur le SIREN 787120435 (Bio Ard'Aisne, SAS Rethel) : présent dans SIRENE
204
+ * via la "fabrique social.gouv" mais absent de l'API DINUM publique.
205
+ * Pour ce cas d'usage, fallback : interroger SIRENE INSEE directement (avec
206
+ * authentification API), ou utiliser `entreprises_in_radius` par zone géo.
207
+ */
208
+ declare function getEntrepriseBySiren(siren: string, signal?: AbortSignal): Promise<LookupResult<Entreprise>>;
209
+
210
+ /**
211
+ * INSEE SIRENE V3.11 — fallback authentifié pour les SIREN absents de DINUM.
212
+ *
213
+ * Pourquoi : `recherche-entreprises.api.gouv.fr` (DINUM) exclut les entreprises
214
+ * en diffusion partielle INSEE (`statut_diffusion ∈ {P,N}`). Cas connu : SIREN
215
+ * 787120435 (BIO ARD'AISNE) présent dans SIRENE mais absent de DINUM. Pour ces
216
+ * SIREN, on fallback sur l'API SIRENE INSEE directement (clé requise).
217
+ *
218
+ * Auth (vérifié 2026-05-09 sur portail-api.insee.fr V3.11) :
219
+ * - Header **`X-INSEE-Api-Key-Integration: <api-key>`** (UUID issu du portail).
220
+ * Bearer / apikey / X-Gravitee-Api-Key tous renvoient 401 — c'est le custom
221
+ * header Gravitee configuré côté gateway INSEE qui prime.
222
+ * - Endpoint : `GET https://api.insee.fr/api-sirene/3.11/siren/{siren}`
223
+ * - Rate limit : 30 req/min (header `x-rate-limit-limit: 30`).
224
+ *
225
+ * Payload V3.11 : les champs métier (denomination, nom, prénom, NAF, état
226
+ * administratif, catégorie juridique) sont dans
227
+ * `uniteLegale.periodesUniteLegale[0]` (la période la plus récente, ordre
228
+ * antéchronologique), PAS sur `uniteLegale` directement.
229
+ *
230
+ * No-op gracieux : si `INSEE_SIRENE_API_KEY` n'est pas configurée,
231
+ * `lookupSirenViaInsee` retourne null sans throw — la lib reste utilisable
232
+ * sans clé INSEE.
233
+ */
234
+
235
+ /**
236
+ * Lit `INSEE_SIRENE_API_KEY` depuis l'env. Retourne `null` si absente ou vide
237
+ * (no-op gracieux : la lib reste utilisable sans clé INSEE).
238
+ *
239
+ * Strippe aussi les guillemets entourants — certains parsers `.env` (ou un
240
+ * copier-coller Vercel UI) les conservent, et l'API INSEE rejette alors
241
+ * silencieusement la clé en 401, ce qui ressemble à une clé révoquée.
242
+ */
243
+ declare function getInseeApiKey(): string | null;
244
+ /**
245
+ * Récupère une entreprise par SIREN via l'API SIRENE INSEE V3.11.
246
+ *
247
+ * Comportement :
248
+ * - Pas de clé configurée → `null` (no-op gracieux, pas de throw)
249
+ * - HTTP 404 → `null` (vraiment pas dans SIRENE)
250
+ * - HTTP 401/403 → `null` + `console.error` (clé invalide ou révoquée)
251
+ * - HTTP 5xx / timeout / erreur réseau → `null` + `console.error`
252
+ * - HTTP 200 → `Entreprise` mappée minimale (siren, nomComplet, naf, actif)
253
+ *
254
+ * ⚠️ Rate limit INSEE : 30 req/min. `fetchJson` retry sur 429 en respectant
255
+ * `retry-after`, mais en burst soutenu (>30 lookups/min) les retries se
256
+ * sérialisent et la latence p99 explose. Conçu comme fallback ponctuel sur
257
+ * SIREN diffusion partielle (cas rare ~1% des SIREN), pas comme source primaire.
258
+ */
259
+ declare function lookupSirenViaInsee(siren: string): Promise<Entreprise | null>;
260
+
261
+ /**
262
+ * Cache fichier local pour les dumps publics téléchargés (FINESS, Annuaire Ameli, INSEE…).
263
+ *
264
+ * Stratégie : un fichier de cache par dataset, refresh si plus vieux que `ttlMs`.
265
+ * Pas de coordination multi-process — si deux processus tentent de refresh en
266
+ * parallèle, le second écrase le premier (acceptable pour des dumps de référence
267
+ * publics).
268
+ *
269
+ * Localisation par défaut : `~/.cache/france-data-mcp/` (suit XDG-ish sur macOS/Linux).
270
+ */
271
+ type CacheOptions = {
272
+ /** Dossier où stocker les caches (défaut : ~/.cache/france-data-mcp) */
273
+ cacheDir?: string;
274
+ /** Durée de vie en millisecondes avant refresh */
275
+ ttlMs?: number;
276
+ /** Forcer le refresh même si le cache est encore valide */
277
+ force?: boolean;
278
+ /** User-Agent à envoyer pour le téléchargement */
279
+ userAgent?: string;
280
+ signal?: AbortSignal;
281
+ };
282
+
283
+ /**
284
+ * FINESS — Fichier National des Établissements Sanitaires et Médico-Sociaux.
285
+ *
286
+ * Source : data.gouv.fr → dump CSV bimestriel ~35 Mo (`finess-extraction-du-fichier-des-etablissements`).
287
+ * Variante géolocalisée : Atlasanté `referentiel-finess-t-finess` (~232 Mo).
288
+ *
289
+ * ⚠️ Migration ANS été 2026 : tous les datasets data.gouv portent l'avertissement
290
+ * que la génération du flux actuel s'arrêtera. Surveiller le repo
291
+ * github.com/ansforge/finess pour le nouveau format (probablement FHIR-compatible).
292
+ *
293
+ * Cette fonction télécharge le CSV avec cache 7j puis charge en mémoire (~35 Mo
294
+ * → ~70 Mo de RAM résidente Node). Adapté à un usage CLI ou serveur node long.
295
+ * Pour un usage serverless (Vercel Edge), ne pas charger l'intégralité —
296
+ * préférer une DB externe (PostGIS, DuckDB).
297
+ */
298
+
299
+ type EtablissementFiness = {
300
+ /** Numéro FINESS de l'entité géographique (ET) sur 9 chiffres */
301
+ finessEt: string;
302
+ /** Numéro FINESS de l'entité juridique (EJ) à laquelle l'ET est rattaché */
303
+ finessEj?: string;
304
+ /** Raison sociale (longue, plus complète que le nom court) */
305
+ raisonSociale: string;
306
+ /** Code de catégorie d'établissement (ex: "500" pour EHPAD) */
307
+ categorieCode?: string;
308
+ /** Libellé de la catégorie (mappé depuis FINESS_CATEGORIES si reconnu) */
309
+ categorieLibelle?: string;
310
+ /** Adresse ligne complète */
311
+ adresse?: string;
312
+ /** Code postal */
313
+ codePostal?: string;
314
+ /** Commune */
315
+ commune?: string;
316
+ /** Code INSEE de la commune (5 caractères) */
317
+ codeCommune?: string;
318
+ /** Code département (2 ou 3 caractères) */
319
+ departement?: string;
320
+ /** Coordonnées GPS (présentes dans le dump géolocalisé Atlasanté) */
321
+ point?: Coordinates;
322
+ /** Téléphone */
323
+ telephone?: string;
324
+ /** SIREN si renseigné */
325
+ siren?: string;
326
+ };
327
+ type LoadFinessOptions = CacheOptions & {
328
+ /**
329
+ * Chemin local d'un CSV déjà téléchargé (court-circuite le download).
330
+ *
331
+ * @security Cette option fait un `readFile` direct du chemin fourni. Ne
332
+ * JAMAIS la forwarder depuis une entrée non-trustée (requête HTTP, args MCP) :
333
+ * c'est un read fichier local non restreint. Strictement réservé à un usage
334
+ * Node.js trusted.
335
+ */
336
+ csvPath?: string;
337
+ };
338
+ type SearchFinessOptions = {
339
+ /** Filtre par codes de catégorie (ex: ["500"] pour EHPAD seuls) */
340
+ categories?: string[];
341
+ /** Filtre par code postal exact */
342
+ codePostal?: string;
343
+ /** Filtre par code département */
344
+ departement?: string;
345
+ /** Filtre par code commune INSEE */
346
+ codeCommune?: string;
347
+ /** Recherche géographique : centre + rayon en km (nécessite dump géolocalisé) */
348
+ center?: Coordinates;
349
+ /** Rayon en km */
350
+ radiusKm?: number;
351
+ /** Limite de résultats (défaut tous) */
352
+ limit?: number;
353
+ };
354
+ /**
355
+ * Charge l'index FINESS en mémoire. Télécharge le CSV si pas en cache.
356
+ * Le résultat est utilisable plusieurs fois sans re-charger.
357
+ */
358
+ declare function loadFiness(options?: LoadFinessOptions): Promise<EtablissementFiness[]>;
359
+ /**
360
+ * Recherche des établissements dans un index FINESS pré-chargé.
361
+ * Pour avoir l'index : `const index = await loadFiness();`
362
+ */
363
+ declare function searchEtablissementsFiness(index: EtablissementFiness[], options: SearchFinessOptions): EtablissementFiness[];
364
+ /**
365
+ * Distance Haversine entre deux points GPS, en mètres.
366
+ * Formule sphérique standard (suffisante pour les rayons < 100 km).
367
+ */
368
+ declare function haversineDistance(a: Coordinates, b: Coordinates): number;
369
+
370
+ /**
371
+ * Annuaire Santé Ameli — répertoire des professionnels de santé libéraux conventionnés.
372
+ *
373
+ * Source : data.gouv.fr `annuaire-sante-ameli` (CSV ~146 Mo, ~1,5M lignes).
374
+ * MAJ hebdomadaire (régénération chaque dimanche/lundi).
375
+ *
376
+ * ⚠️ Article L.1461-2 CSP : ces données contiennent des informations à
377
+ * caractère personnel. Leur réutilisation est soumise au respect de la
378
+ * réglementation relative à la protection de la vie privée. Toute application
379
+ * publique doit afficher la mention "Source : Annuaire santé Ameli, Assurance
380
+ * Maladie" et la date de la dernière sync.
381
+ *
382
+ * Volume : 146 Mo → trop pour charger en mémoire intégralement. Cette lib
383
+ * propose un parser **streaming** : on lit le CSV ligne par ligne et on filtre
384
+ * à la volée. Pour faire de la recherche par rayon géographique, il faut au
385
+ * préalable géocoder les adresses (à faire côté caller, pas géré ici).
386
+ */
387
+
388
+ type ProfessionnelSante = {
389
+ /** Nom de famille (en exercice) */
390
+ nom: string;
391
+ /** Prénom(s) (en exercice) */
392
+ prenom: string;
393
+ /** Civilité (Dr, Mme, M.…) */
394
+ civilite?: string;
395
+ /** Raison sociale du lieu d'exercice si applicable */
396
+ raisonSociale?: string;
397
+ /** Code spécialité Ameli */
398
+ specialiteCode?: string;
399
+ /** Libellé de la spécialité (ex: "Médecin généraliste", "Cardiologue") */
400
+ specialiteLibelle?: string;
401
+ /** Code type de PS (médecin, IDE, sage-femme, pharmacien…) */
402
+ typePsCode?: string;
403
+ /** Libellé du type de PS */
404
+ typePsLibelle?: string;
405
+ /** Voie + numéro */
406
+ adresse?: string;
407
+ /** Complément d'adresse (étage, bâtiment…) */
408
+ complementAdresse?: string;
409
+ /** Code postal du lieu d'exercice */
410
+ codePostal?: string;
411
+ /** Commune du lieu d'exercice */
412
+ commune?: string;
413
+ /** Téléphone */
414
+ telephone?: string;
415
+ /** Secteur conventionnel (1, 2, 3, NC…) */
416
+ secteurConventionnel?: string;
417
+ /** Libellé du secteur conventionnel */
418
+ secteurConventionnelLibelle?: string;
419
+ /** Mode d'exercice (libéral, salarié, mixte…) */
420
+ natureExercice?: string;
421
+ };
422
+ type StreamAnnuaireOptions = CacheOptions & {
423
+ /**
424
+ * Chemin local d'un CSV déjà téléchargé (court-circuite le download).
425
+ *
426
+ * @security Cette option ouvre un `createReadStream` direct sur le chemin
427
+ * fourni. Ne JAMAIS la forwarder depuis une entrée non-trustée (requête
428
+ * HTTP, args MCP) : c'est une lecture fichier local non restreinte qui peut
429
+ * exposer des fichiers sensibles. Strictement réservé à un usage Node.js
430
+ * trusted (CLI, script, code applicatif).
431
+ */
432
+ csvPath?: string;
433
+ };
434
+ type FilterAnnuaireOptions = {
435
+ /** Filtre exact par code postal */
436
+ codePostal?: string;
437
+ /** Filtre par préfixe de code postal (ex: "08" pour tout le département 08) */
438
+ codePostalPrefix?: string;
439
+ /** Filtre par nom de commune (insensible à la casse) */
440
+ commune?: string;
441
+ /** Filtre par libellé de spécialité (insensible à la casse, contient) */
442
+ specialite?: string;
443
+ /** Filtre par code spécialité exact */
444
+ specialiteCode?: string;
445
+ /** Filtre par type de PS (Médecin, IDE, etc., insensible à la casse) */
446
+ typePs?: string;
447
+ /** Filtre par secteur conventionnel ("1", "2"…) */
448
+ secteurConventionnel?: string;
449
+ /** Limite (arrête le stream une fois atteinte) */
450
+ limit?: number;
451
+ };
452
+ /**
453
+ * S'assure que le CSV Annuaire Ameli est en cache local et renvoie son chemin.
454
+ * Télécharge si nécessaire (~146 Mo, ~30 secondes en bonne connexion).
455
+ */
456
+ declare function ensureAnnuaireAmeli(options?: StreamAnnuaireOptions): Promise<string>;
457
+ /**
458
+ * Stream les professionnels de santé un par un, avec filtres optionnels.
459
+ * Utilise un parser CSV streaming pour ne pas charger les 146 Mo en mémoire.
460
+ *
461
+ * @example Tous les MG du 08
462
+ * ```ts
463
+ * const out: string[] = [];
464
+ * for await (const ps of streamProfessionnels({ codePostalPrefix: "08", specialite: "généraliste" })) {
465
+ * out.push(`${ps.nom} ${ps.prenom} - ${ps.commune}`);
466
+ * }
467
+ * ```
468
+ */
469
+ declare function streamProfessionnels(options?: StreamAnnuaireOptions & FilterAnnuaireOptions): AsyncGenerator<ProfessionnelSante>;
470
+ /**
471
+ * Charge un sous-ensemble filtré en mémoire (pratique pour les zones géographiques
472
+ * étroites — un département entier reste raisonnable).
473
+ */
474
+ declare function loadProfessionnels(options?: StreamAnnuaireOptions & FilterAnnuaireOptions): Promise<ProfessionnelSante[]>;
475
+
476
+ /**
477
+ * Mapping des codes NAF utiles pour l'analyse santé / médico-social.
478
+ *
479
+ * Source : nomenclature NAF rév.2 INSEE (2008).
480
+ * Liste non exhaustive — focus sur les activités santé pertinentes pour
481
+ * l'intelligence territoriale (implantation, prospection, audit).
482
+ */
483
+ declare const NAF_SANTE: {
484
+ readonly "8610Z": "Activités hospitalières";
485
+ readonly "8621Z": "Activités de médecine générale";
486
+ readonly "8622A": "Activités de radiodiagnostic et de radiothérapie";
487
+ readonly "8622B": "Activités chirurgicales";
488
+ readonly "8622C": "Autres activités des médecins spécialistes";
489
+ readonly "8623Z": "Pratique dentaire";
490
+ readonly "8690A": "Ambulances";
491
+ readonly "8690B": "Laboratoires d'analyses médicales";
492
+ readonly "8690C": "Centres de collecte et banques d'organes";
493
+ readonly "8690D": "Activités des infirmiers et des sages-femmes";
494
+ readonly "8690E": "Activités des professionnels de la rééducation, de l'appareillage et des pédicures-podologues";
495
+ readonly "8690F": "Activités de santé humaine non classées ailleurs";
496
+ readonly "8710A": "Hébergement médicalisé pour personnes âgées";
497
+ readonly "8710B": "Hébergement médicalisé pour enfants handicapés";
498
+ readonly "8710C": "Hébergement médicalisé pour adultes handicapés et autre hébergement médicalisé";
499
+ readonly "8720A": "Hébergement social pour handicapés mentaux et malades mentaux";
500
+ readonly "8720B": "Hébergement social pour toxicomanes";
501
+ readonly "8730A": "Hébergement social pour personnes âgées";
502
+ readonly "8730B": "Hébergement social pour handicapés physiques";
503
+ readonly "8810A": "Aide à domicile";
504
+ readonly "8810B": "Accueil ou accompagnement sans hébergement d'adultes handicapés ou de personnes âgées";
505
+ readonly "8810C": "Aide par le travail";
506
+ readonly "8891A": "Accueil de jeunes enfants";
507
+ readonly "8891B": "Accueil ou accompagnement sans hébergement d'enfants handicapés";
508
+ readonly "8899A": "Autre accueil ou accompagnement sans hébergement d'enfants et d'adolescents";
509
+ readonly "8899B": "Action sociale sans hébergement n.c.a.";
510
+ readonly "4773Z": "Commerce de détail de produits pharmaceutiques en magasin spécialisé";
511
+ readonly "4774Z": "Commerce de détail d'articles médicaux et orthopédiques en magasin spécialisé";
512
+ };
513
+ type NafCodeSante = keyof typeof NAF_SANTE;
514
+ /**
515
+ * Codes NAF correspondant aux laboratoires de biologie médicale et activités proches.
516
+ */
517
+ declare const NAF_LABOS: readonly ["8690B"];
518
+ /**
519
+ * Codes NAF correspondant aux pharmacies d'officine.
520
+ */
521
+ declare const NAF_PHARMACIES: readonly ["4773Z"];
522
+ /**
523
+ * Codes NAF correspondant aux EHPAD et hébergement médicalisé pour personnes âgées.
524
+ */
525
+ declare const NAF_EHPAD: readonly ["8710A", "8730A"];
526
+ /**
527
+ * Codes NAF correspondant à la médecine de ville (généralistes + spécialistes).
528
+ */
529
+ declare const NAF_MEDECINE_VILLE: readonly ["8621Z", "8622A", "8622B", "8622C"];
530
+ /**
531
+ * Renvoie le libellé d'un code NAF santé, ou undefined si non répertorié.
532
+ */
533
+ declare function libelleNaf(code: string): string | undefined;
534
+
535
+ /**
536
+ * FINESS DREES category nomenclature — codes + libellés.
537
+ *
538
+ * Catalogue ~50 codes représentant ~92% du volume FINESS (95K rows total).
539
+ * Le reliquat ~8% tombe en famille `autre` (codes très rares : thermal,
540
+ * lieux de vie expérimentaux, structures atypiques).
541
+ *
542
+ * Source: live FINESS extract on data.gouv.fr. Re-verify against the CSV
543
+ * when adding codes — DREES occasionally rotates labels (les libellés
544
+ * sont copiés à l'identique du CSV pour matcher exactement la nomenclature
545
+ * officielle).
546
+ */
547
+ declare const FINESS_CATEGORIES: {
548
+ readonly "101": "Centre Hospitalier Régional (C.H.R.)";
549
+ readonly "106": "Centre hospitalier";
550
+ readonly "108": "Centre Hospitalier Universitaire (C.H.U.)";
551
+ readonly "114": "Hôpital des armées";
552
+ readonly "115": "Etablissement de Soins du Service de Santé des Armées";
553
+ readonly "128": "Etablissement de Soins Chirurgicaux";
554
+ readonly "129": "Etablissement de Soins Médicaux";
555
+ readonly "131": "Centre de Lutte Contre le Cancer (C.L.C.C.)";
556
+ readonly "355": "Centre Hospitalier (C.H.)";
557
+ readonly "365": "Etablissement de Soins Pluridisciplinaire";
558
+ readonly "109": "Etablissement de santé privé autorisé en SSR";
559
+ readonly "362": "Etablissement de Soins Longue Durée (USLD)";
560
+ readonly "127": "Hospitalisation à Domicile (HAD)";
561
+ readonly "141": "Centre de dialyse";
562
+ readonly "146": "Structure d'Alternative à la dialyse en centre";
563
+ readonly "292": "Centre Hospitalier Spécialisé lutte Maladies Mentales";
564
+ readonly "156": "Centre Médico-Psychologique (C.M.P.)";
565
+ readonly "161": "Maison de Santé pour Maladies Mentales";
566
+ readonly "425": "Centre d'Accueil Thérapeutique à temps partiel (C.A.T.T.P.)";
567
+ readonly "430": "Centre Postcure Malades Mentaux";
568
+ readonly "124": "Centre de Santé";
569
+ readonly "603": "Maison de santé (L.6223-3)";
570
+ readonly "604": "Communautés professionnelles territoriales de santé (CPTS)";
571
+ readonly "611": "Laboratoire de Biologie Médicale";
572
+ readonly "619": "Cabinet d'imagerie médicale";
573
+ readonly "620": "Pharmacie d'Officine";
574
+ readonly "627": "Propharmacie";
575
+ readonly "500": "Etablissement d'hébergement pour personnes âgées dépendantes (EHPAD)";
576
+ readonly "501": "EHPA percevant des crédits d'assurance maladie";
577
+ readonly "502": "EHPA ne percevant pas des crédits d'assurance maladie";
578
+ readonly "202": "Résidences autonomie";
579
+ readonly "207": "Centre de Jour pour Personnes Agées";
580
+ readonly "463": "Centres Locaux Information Coordination P.A. (C.L.I.C.)";
581
+ readonly "354": "Service de Soins Infirmiers à Domicile (S.S.I.A.D.)";
582
+ readonly "460": "Service d'Aide et d'Accompagnement à Domicile (S.A.A.D.)";
583
+ readonly "209": "Service Polyvalent Aide et Soins à Domicile (S.P.A.S.A.D.)";
584
+ readonly "182": "Service d'Éducation Spéciale et de Soins à Domicile (SESSAD)";
585
+ readonly "183": "Institut Médico-Éducatif (I.M.E.)";
586
+ readonly "186": "Institut Thérapeutique Éducatif et Pédagogique (I.T.E.P.)";
587
+ readonly "188": "Etablissement pour Enfants ou Adolescents Polyhandicapés";
588
+ readonly "189": "Centre Médico-Psycho-Pédagogique (C.M.P.P.)";
589
+ readonly "190": "Centre Action Médico-Sociale Précoce (C.A.M.S.P.)";
590
+ readonly "192": "Institut d'éducation motrice";
591
+ readonly "194": "Institut pour Déficients Visuels";
592
+ readonly "195": "Institut pour Déficients Auditifs";
593
+ readonly "196": "Institut d'Education Sensorielle Sourd/Aveugle";
594
+ readonly "246": "Etablissement et Service d'Aide par le Travail (E.S.A.T.)";
595
+ readonly "247": "Entreprise adaptée";
596
+ readonly "252": "Foyer Hébergement Adultes Handicapés";
597
+ readonly "255": "Maison d'Accueil Spécialisée (M.A.S.)";
598
+ readonly "382": "Foyer de Vie pour Adultes Handicapés";
599
+ readonly "437": "Foyer d'Accueil Médicalisé pour Adultes Handicapés (F.A.M.)";
600
+ readonly "445": "Service d'accompagnement médico-social adultes handicapés (SAMSAH)";
601
+ readonly "446": "Service d'Accompagnement à la Vie Sociale (S.A.V.S.)";
602
+ readonly "448": "Etab. Acc. Médicalisé en tout ou partie personnes handicapées";
603
+ readonly "449": "Etab. Accueil Non Médicalisé pour personnes handicapées";
604
+ readonly "600": "Foyer d'hébergement pour adultes handicapés";
605
+ readonly "165": "Appartement de Coordination Thérapeutique (A.C.T.)";
606
+ readonly "178": "Centre Accueil/Accomp. Réduc. Risq. Usag. Drogues (C.A.A.R.U.D.)";
607
+ readonly "180": "Lits Halte Soins Santé (L.H.S.S.)";
608
+ readonly "197": "Centre soins accompagnement prévention addictologie (C.S.A.P.A.)";
609
+ readonly "412": "Appartement Thérapeutique";
610
+ readonly "175": "Foyer de l'Enfance";
611
+ readonly "177": "Maison d'Enfants à Caractère Social (MECS)";
612
+ readonly "295": "Services AEMO et AED";
613
+ readonly "236": "Centre Placement Familial Socio-Educatif (C.P.F.S.E.)";
614
+ readonly "238": "Centre d'Accueil Familial Spécialisé";
615
+ readonly "241": "Foyer d'Action Educative (F.A.E.)";
616
+ readonly "440": "Service Investigation Orientation Educative (S.I.O.E.)";
617
+ readonly "441": "Centre d'Action Educative (C.A.E.)";
618
+ readonly "378": "Etablissement Expérimental Enfance Protégée";
619
+ readonly "223": "Protection Maternelle et Infantile (P.M.I.)";
620
+ readonly "228": "Centre Planification ou Education Familiale";
621
+ readonly "230": "Etablissement Consultation Protection Infantile";
622
+ readonly "268": "Centre Médico-Scolaire";
623
+ readonly "214": "Centre Hébergement & Réinsertion Sociale (C.H.R.S.)";
624
+ readonly "219": "Autre Centre d'Accueil";
625
+ readonly "256": "Foyer Travailleurs Migrants non transformé en Résidence Sociale";
626
+ readonly "257": "Foyer de Jeunes Travailleurs (résidence sociale ou non)";
627
+ readonly "258": "Maisons Relais - Pensions de Famille";
628
+ readonly "259": "Autre Résidence Sociale (hors Maison Relais)";
629
+ readonly "443": "Centre Accueil Demandeurs Asile (C.A.D.A.)";
630
+ readonly "442": "Centre Provisoire Hébergement (C.P.H.)";
631
+ readonly "462": "Lieux de vie";
632
+ readonly "166": "Etablissement d'Accueil Mère-Enfant";
633
+ readonly "132": "Etablissement de Transfusion Sanguine";
634
+ readonly "142": "Dispensaire Antituberculeux";
635
+ readonly "143": "Centre de Vaccination BCG";
636
+ readonly "266": "Dispensaire Antivénérien";
637
+ readonly "347": "Centre d'Examens de Santé";
638
+ readonly "636": "Centre de soins et de prévention";
639
+ readonly "696": "Groupement de coopération sanitaire de moyens";
640
+ readonly "697": "Groupement de coopération sanitaire — Etablissement de santé";
641
+ readonly "126": "Etablissement Thermal";
642
+ readonly "632": "Structure Dispensatrice à domicile d'Oxygène à usage médical";
643
+ readonly "698": "Autre Etablissement Loi Hospitalière";
644
+ };
645
+ type FinessCategorieCode = keyof typeof FINESS_CATEGORIES;
646
+ declare function libelleCategorieFiness(code: string): string | undefined;
647
+ /**
648
+ * FINESS family taxonomy. Drives the `familles` filter on the MCP tools.
649
+ *
650
+ * Each family is a precise, query-side tag — callers compose them via the
651
+ * `familles` array. Designed for prospection commerciale santé : labos
652
+ * ciblent MCO/EHPAD/CSI/MSP/CPTS/MAS/FAM/SLD/HAD, équipementiers ciblent
653
+ * EHPAD/résidences autonomie, services à domicile ciblent SAAD/SPASAD/SSIAD…
654
+ */
655
+ type FinessFamille = "mco" | "ssr" | "sld" | "had" | "psychiatrie" | "dialyse" | "ambulatoire" | "labo" | "imagerie" | "pharmacie" | "msp_cpts" | "ehpad" | "residence_autonomie" | "senior_accompagnement" | "ssiad" | "aide_domicile" | "handicap_enfants" | "handicap_adultes" | "addictologie" | "enfance_protection" | "pmi" | "hebergement_social" | "prevention_sante" | "groupement" | "autre";
656
+ /**
657
+ * Family classification of FINESS DREES category codes.
658
+ *
659
+ * `autre` is the catch-all : codes in FINESS_CATEGORIES that don't fit any
660
+ * specific family (ex. thermal). To get "everything else", omit the family
661
+ * filter and post-filter via `result.categorie.famille`.
662
+ *
663
+ * The `query` subtype excludes "autre" — the MCP tools accept this set for
664
+ * the `familles` parameter.
665
+ */
666
+ type FinessFamilleQuery = Exclude<FinessFamille, "autre">;
667
+ declare const FINESS_FAMILY_CODES: Record<FinessFamilleQuery, readonly string[]>;
668
+ /**
669
+ * Classify a FINESS category code into a family for query-side filtering.
670
+ *
671
+ * Inputs are normalized first: `null`, `undefined`, empty string, and
672
+ * whitespace-only strings all resolve to "autre". Non-empty inputs are trimmed
673
+ * before matching, tolerating whitespace artefacts occasionally present in
674
+ * DREES dumps (e.g. `" 108 "` → "mco").
675
+ */
676
+ declare function finessFamille(code: string | null | undefined): FinessFamille;
677
+ /**
678
+ * All hospital-grade categories (MCO acute-care + SSR + SLD + HAD + psy).
679
+ * Use FINESS_FAMILY_CODES.mco for strict acute-care only.
680
+ */
681
+ declare const FINESS_HOPITAUX: readonly string[];
682
+ declare const FINESS_LABOS: readonly string[];
683
+ declare const FINESS_PHARMACIES: readonly string[];
684
+ declare const FINESS_EHPAD: readonly string[];
685
+ declare const FINESS_MSP_CPTS: readonly string[];
686
+
687
+ /**
688
+ * Métadonnées de requête exposées dans les réponses des tools de listing.
689
+ *
690
+ * Pourquoi : le caller MCP (Claude.ai, Cursor, agent LLM) ne peut pas deviner
691
+ * la nature du calcul de distance ni la précision géographique en lisant un
692
+ * `distance_km: 2.67`. Or les sources varient : FINESS expose des coords
693
+ * Lambert93 reprojetées en WGS84 (précision adresse), Ameli ne fournit que
694
+ * le centroïde commune (~3 km moyenne). Sans cette transparence, un caller
695
+ * peut prendre des décisions logistiques fausses (ex: "le LBM est à 2.67 km
696
+ * vol d'oiseau" — mais la distance routière fait facilement +20-30%).
697
+ *
698
+ * Pattern aligné sur le bloc `fallback` déjà présent dans
699
+ * `entreprises_in_radius` (cf. `api/tools.ts`) qui surface honnêtement la
700
+ * stratégie de fallback API DINUM.
701
+ */
702
+ /**
703
+ * Précision géographique des coordonnées exposées dans les résultats.
704
+ *
705
+ * - `lambert93_natif_finess` : coords FINESS DREES (Lambert 93 reprojeté
706
+ * WGS84 à l'ingestion). Précision adresse ~10 m côté DREES.
707
+ * - `centroide_commune_ameli` : coords Ameli (centroïde commune via
708
+ * `geo.api.gouv.fr/communes`). Précision ~3 km moyenne — adapté à
709
+ * l'analyse de densité, PAS au géocodage adresse.
710
+ */
711
+ type GeoPrecision = "lambert93_natif_finess" | "centroide_commune_ameli" | "centroide_commune_ans" | "structure_finess";
712
+ /**
713
+ * Méthode de calcul des distances exposées dans `distance_km`.
714
+ *
715
+ * - `haversine_postgis` : ST_Distance sur le type `geography` PostGIS.
716
+ * Distance vol d'oiseau, pas routière. Pour la distance routière,
717
+ * intégrer un service externe (OSRM, ORS) côté caller.
718
+ */
719
+ type DistanceType = "haversine_postgis";
720
+ interface QueryMetadata {
721
+ geo_precision: GeoPrecision;
722
+ /** Présent uniquement quand la requête expose `distance_km` (radius). */
723
+ distance_type?: DistanceType;
724
+ /** Notes actionnables pour le caller (précision attendue, cross-checks…). */
725
+ notes: string[];
726
+ }
727
+
728
+ interface FinessResult {
729
+ num_finess: string;
730
+ raison_sociale: string;
731
+ categorie: {
732
+ code: string | null;
733
+ libelle: string | null;
734
+ famille: FinessFamille;
735
+ };
736
+ adresse: {
737
+ voie: string | null;
738
+ code_postal: string | null;
739
+ ville: string | null;
740
+ code_departement: string | null;
741
+ code_insee: string;
742
+ };
743
+ coords: {
744
+ lat: number;
745
+ lon: number;
746
+ } | null;
747
+ distance_km: number | null;
748
+ telephone: string | null;
749
+ email: string | null;
750
+ }
751
+ interface InRadiusInput {
752
+ center: {
753
+ lat: number;
754
+ lon: number;
755
+ };
756
+ radiusKm: number;
757
+ familles?: FinessFamilleQuery[];
758
+ limit?: number;
759
+ }
760
+ interface ByCategorieInput {
761
+ famille: FinessFamilleQuery;
762
+ departement?: string;
763
+ code_insee?: string;
764
+ limit?: number;
765
+ }
766
+ interface FinessQueryResult {
767
+ count: number;
768
+ truncated: boolean;
769
+ results: FinessResult[];
770
+ /**
771
+ * Métadonnées sur la précision géo et le type de distance. Surface au
772
+ * caller MCP que les coords proviennent du Lambert93 DREES (~adresse) et
773
+ * que la distance est haversine (pas routière). Inclut un rappel sur la
774
+ * latence DREES (~1-2 mois) pour les structures émergentes.
775
+ *
776
+ * Optionnel : tous les RPCs de prod la peuplent (cf. `getFinessInRadius`/
777
+ * `getFinessByCategorie`) ; cas d'absence réservé aux mocks tests.
778
+ */
779
+ query_metadata?: QueryMetadata;
780
+ }
781
+ /**
782
+ * Find FINESS establishments within a geographic radius. Spatial query uses
783
+ * PostGIS ST_DWithin on the geography type for accurate kilometers.
784
+ *
785
+ * Implemented via a Postgres RPC (`finess_in_radius`, migration
786
+ * 20260508000004) because supabase-js cannot express ST_DWithin / ST_Distance
787
+ * through its query builder.
788
+ */
789
+ declare function getFinessInRadius(input: InRadiusInput): Promise<FinessQueryResult>;
790
+ /**
791
+ * Find FINESS establishments by family (and optional dept / commune filters).
792
+ * No spatial query — pure WHERE on category code list + optional location.
793
+ */
794
+ declare function getFinessByCategorie(input: ByCategorieInput): Promise<FinessQueryResult>;
795
+ /**
796
+ * Fetch a single FINESS establishment by its 9-digit FINESS number.
797
+ *
798
+ * Retourne un `LookupResult` discriminé par `found`. Si le numéro n'existe
799
+ * pas dans le dump FINESS DREES (numéro mal formé, fermeture récente non
800
+ * encore propagée, frais d'établissement émergent — la base DREES a 1-2 mois
801
+ * de retard sur le terrain), la fonction renvoie un objet `{ found: false,
802
+ * lookupStatus: "not_found", message }` au lieu d'un `null` silencieux.
803
+ * Pattern aligné sur `getEntrepriseBySiren` et `getCommuneByCode`
804
+ * (cf. `src/core/lookup-result.ts`).
805
+ */
806
+ declare function getFinessByNumFiness(numFiness: string): Promise<LookupResult<FinessResult>>;
807
+
808
+ /**
809
+ * Codes mode d'exercice ANS (extrait de la nomenclature canonique).
810
+ * Documenté ici pour clarté du caller MCP qui veut filtrer par statut.
811
+ *
812
+ * Source : nomenclature de structure ANS, fichier `nomenclature-mode-exercice`.
813
+ */
814
+ declare const RPPS_MODE_EXERCICE: {
815
+ readonly LIBERAL: "L";
816
+ readonly SALARIE: "S";
817
+ readonly MIXTE: "M";
818
+ readonly REMPLACANT: "R";
819
+ readonly AUTRE: "A";
820
+ readonly BENEVOLE: "B";
821
+ };
822
+ /**
823
+ * URL de référence ANS pour les nomenclatures publiques. Mention obligatoire
824
+ * en CGU des datasets data.gouv : « Source : Annuaire Santé, ANS — Licence
825
+ * Ouverte v2.0 ».
826
+ */
827
+ declare const RPPS_CGU_NOTICE = "Source : Annuaire Sant\u00E9, Agence du Num\u00E9rique en Sant\u00E9 (ANS) \u2014 Licence Ouverte v2.0";
828
+
829
+ /**
830
+ * RPPS / Annuaire Santé ANS — wrappers typés autour des RPCs PostGIS.
831
+ *
832
+ * Source : data.gouv `annuaire-sante-extractions-...-rpps`, Licence Ouverte v2.0.
833
+ * La mention obligatoire (ANS / Licence Ouverte v2.0) est portée par les
834
+ * descriptions des tools MCP (`api/tools.ts`). Ce module est le boundary
835
+ * technique, pas le boundary public.
836
+ *
837
+ * Diffère d'Ameli sur 3 points :
838
+ * - couverture : libéraux + salariés + étudiants + agents publics
839
+ * (vs Ameli libéraux conventionnés uniquement)
840
+ * - identifiant stable : `rpps_id` (IDNPS national) → lookup individuel + dédup
841
+ * - pivot structure : `num_finess` exposé en colonne → croisement avec FINESS
842
+ *
843
+ * IMPORTANT : la base ne contient QUE des PS actifs. L'ANS pré-filtre le
844
+ * fichier `PS_LibreAcces_Personne_activite` à la source : retraités, décédés,
845
+ * radiés et suspendus n'apparaissent jamais dans cette extraction (cf. DSFT
846
+ * v3.1 §5.1.2). Le filtre par `categorie_code` discrimine donc des **statuts
847
+ * juridiques d'enregistrement** (Civil / Étudiant / Agent public), pas des
848
+ * statuts d'activité.
849
+ */
850
+
851
+ interface RppsResult {
852
+ id: number;
853
+ rpps_id: string;
854
+ identite: {
855
+ nom: string;
856
+ prenom: string;
857
+ civilite: string | null;
858
+ };
859
+ profession: {
860
+ code: string | null;
861
+ libelle: string | null;
862
+ };
863
+ /** Spécialité fine (DES/DESC). Plus riche que la spécialité Ameli simple. */
864
+ savoir_faire: {
865
+ code: string | null;
866
+ libelle: string | null;
867
+ };
868
+ mode_exercice: {
869
+ code: string | null;
870
+ libelle: string | null;
871
+ };
872
+ /** Catégorie professionnelle ANS (TRE_R09) — voir `CATEGORIE_CODE_*` / `buildCategorieCodes`. */
873
+ categorie: {
874
+ code: string | null;
875
+ libelle: string | null;
876
+ };
877
+ /** Pivot vers FINESS / SIRENE. Souvent rempli pour les salariés, plus rare en libéral pur. */
878
+ structure: {
879
+ num_finess: string | null;
880
+ num_finess_ej: string | null;
881
+ siret: string | null;
882
+ raison_sociale: string | null;
883
+ };
884
+ adresse: {
885
+ voie: string | null;
886
+ code_postal: string | null;
887
+ ville: string | null;
888
+ code_departement: string | null;
889
+ code_insee: string | null;
890
+ };
891
+ coords: {
892
+ lat: number;
893
+ lon: number;
894
+ } | null;
895
+ distance_km: number | null;
896
+ telephone: string | null;
897
+ /**
898
+ * Score de pertinence trigram (0..1) — présent uniquement pour les retours
899
+ * de `rpps_search_by_name`. Permet au caller de filtrer les homonymies
900
+ * partielles (typiquement `< 0.5`).
901
+ */
902
+ match_score?: number;
903
+ }
904
+ interface RppsLookupResult extends RppsResult {
905
+ /** Identifiant PP legacy (pré-IDNPS), conservé quand fourni par l'extract. */
906
+ identifiant_pp: string | null;
907
+ siren: string | null;
908
+ email: string | null;
909
+ }
910
+ interface RppsInRadiusInput {
911
+ center: {
912
+ lat: number;
913
+ lon: number;
914
+ };
915
+ radiusKm: number;
916
+ /** Codes profession ANS (ex: "10" Médecin, "60" Infirmier). */
917
+ professionCodes?: string[];
918
+ /** Codes savoir-faire (DES/DESC). Granularité fine. */
919
+ savoirFaireCodes?: string[];
920
+ /** Codes mode exercice (L libéral, S salarié, M mixte, R remplaçant…). */
921
+ modeExerciceCodes?: string[];
922
+ /**
923
+ * Codes catégorie professionnelle ANS (table TRE_R09). Vide ou omis →
924
+ * filtre default = `[CATEGORIE_CODE_CIVIL]` (cf. `buildCategorieCodes`).
925
+ * Sinon → filtre exact ANY (le helper SQL `rpps_categorie_match` ajoute
926
+ * `OR IS NULL` défensif pour ne pas exclure les rows à code absent).
927
+ */
928
+ categorieCodes?: string[];
929
+ limit?: number;
930
+ }
931
+ interface RppsParSpecialiteDeptInput {
932
+ departement: string;
933
+ professionCode?: string;
934
+ savoirFaireCode?: string;
935
+ modeExerciceCode?: string;
936
+ /** Voir `RppsInRadiusInput.categorieCodes`. */
937
+ categorieCodes?: string[];
938
+ limit?: number;
939
+ offset?: number;
940
+ }
941
+ interface RppsDansEtablissementInput {
942
+ /** Numéro FINESS (9 chiffres) du site d'exercice. */
943
+ numFiness: string;
944
+ /** Voir `RppsInRadiusInput.categorieCodes`. */
945
+ categorieCodes?: string[];
946
+ limit?: number;
947
+ }
948
+ interface RppsQueryResult {
949
+ count: number;
950
+ truncated: boolean;
951
+ results: RppsResult[];
952
+ query_metadata?: QueryMetadata;
953
+ }
954
+ declare function getRppsInRadius(input: RppsInRadiusInput): Promise<RppsQueryResult>;
955
+ declare function getRppsParSpecialiteDept(input: RppsParSpecialiteDeptInput): Promise<RppsQueryResult>;
956
+ /** "Qui travaille dans ce FINESS ?" — lit la colonne indexée `num_finess`. */
957
+ declare function getRppsDansEtablissement(input: RppsDansEtablissementInput): Promise<RppsQueryResult>;
958
+ /**
959
+ * Lookup individuel par RPPS ID. Renvoie N rows quand un PS multi-sites
960
+ * existe (1 ligne par site). Le caller MCP aplatit en `(rpps_id, sites[])`.
961
+ */
962
+ declare function getRppsById(rppsId: string): Promise<RppsLookupResult[]>;
963
+
964
+ /**
965
+ * Annuaire Santé ANS — fallback FHIR live (libre accès, depuis avril 2025).
966
+ *
967
+ * Pourquoi : la table `rpps` est ingérée mensuellement depuis le CSV
968
+ * data.gouv. Pour les SP qui ne sont pas encore dans le snapshot DB (récente
969
+ * inscription, mutation de structure non répercutée), on offre un fallback
970
+ * live via l'API FHIR ANS qui est rafraîchie quotidiennement côté ANS.
971
+ *
972
+ * Auth (vérifié 2026-05-09 sur portail.openfhir.annuaire.sante.fr) :
973
+ * - Header **`ESANTE-API-KEY: <api-key>`** (UUID issu d'une souscription
974
+ * Gravitee gratuite sur portal.api.esante.gouv.fr).
975
+ * - Endpoint : `GET https://gateway.api.esante.gouv.fr/fhir/v2/Practitioner?identifier=...`
976
+ * - Pas de quota documenté pendant la bêta (publique depuis avril 2025) ;
977
+ * limits annoncées « après fin 2025 ».
978
+ *
979
+ * Identifiants : l'API expose `Practitioner.identifier` typé `IDNPS`
980
+ * (système OID `urn:oid:1.2.250.1.71.4.2.1`). Le CSV legacy expose le
981
+ * même champ sous "Identification nationale PP" (11 ou 12 chars selon que
982
+ * le préfixe Type d'identifiant `81` est concaténé ou pas — IDNPS modernes
983
+ * = 12 chars, anciens = 11). On cherche par IDNPS qui matche le `rpps_id`
984
+ * que l'on stocke en DB.
985
+ *
986
+ * No-op gracieux : pas de clé → null sans throw — la lib reste utilisable
987
+ * sans clé ANS (la couverture DB suffit à la majorité des cas).
988
+ */
989
+ declare function getAnsFhirApiKey(): string | null;
990
+ /** Override optionnel (env de staging, mock test). Sinon endpoint officiel. */
991
+ declare function getAnsFhirBaseUrl(): string;
992
+ /**
993
+ * Résultat aplati d'un lookup ANS — surface minimaliste pour ne pas dupliquer
994
+ * la richesse FHIR. Le caller MCP (tool `professionnel_by_rpps`) injecte
995
+ * cette shape quand la DB ne trouve pas le PS.
996
+ */
997
+ interface AnsFhirPractitioner {
998
+ /** ID interne ANS (format `003-NNNN-NNNN`). Distinct de l'IDNPS. */
999
+ ans_internal_id: string;
1000
+ /** IDNPS / RPPS national (11 ou 12 chars selon génération). Identique à `rpps_id` côté DB. */
1001
+ rpps_id: string;
1002
+ civilite: string | null;
1003
+ nom: string;
1004
+ prenom: string;
1005
+ active: boolean | null;
1006
+ source: "ans_fhir";
1007
+ }
1008
+ /**
1009
+ * Résultat discriminé d'un lookup ANS FHIR. **V0.7.0 breaking** — avant V0.7.0,
1010
+ * la fonction retournait `null` indifféremment pour 4 cas (pas de clé, format
1011
+ * invalide, PS absent, API down), masquant l'information critique au caller.
1012
+ *
1013
+ * - `found: true` → `practitioner` peuplé, source ANS live.
1014
+ * - `status: "no_key"` → clé `ESANTE-API-KEY` non configurée côté serveur.
1015
+ * Fallback indisponible — le caller doit s'appuyer sur la DB locale.
1016
+ * - `status: "invalid_format"` → `rpps_id` rejeté par la garde format
1017
+ * (11 ou 12 chiffres requis). Pas d'I/O réseau.
1018
+ * - `status: "not_found"` → ANS a répondu, PS réellement absent (Bundle vide
1019
+ * ou 404). Retry inutile, PS pas inscrit à l'ANS.
1020
+ * - `status: "api_error"` → ANS indisponible (5xx, timeout, 401, 403, network).
1021
+ * Retry justifié dans quelques minutes.
1022
+ */
1023
+ type AnsFhirLookupResult = {
1024
+ found: true;
1025
+ practitioner: AnsFhirPractitioner;
1026
+ } | {
1027
+ found: false;
1028
+ status: "no_key" | "invalid_format" | "not_found" | "api_error";
1029
+ message: string;
1030
+ };
1031
+ /**
1032
+ * Lookup FHIR ANS par IDNPS / rpps_id. Utilisé en fallback du lookup DB.
1033
+ * Latence p99 ~1-2s. Pas de cache local (ANS est rafraîchi quotidiennement).
1034
+ *
1035
+ * Retourne un `AnsFhirLookupResult` discriminé — voir le type pour les 5 cas
1036
+ * possibles. **Aucun `null` silencieux** (V0.7.0 breaking, cf. JSDoc du type).
1037
+ */
1038
+ declare function lookupPractitionerByRpps(rppsId: string): Promise<AnsFhirLookupResult>;
1039
+
1040
+ /**
1041
+ * Module sante — données françaises de santé publique.
1042
+ *
1043
+ * Sources :
1044
+ * - DINUM Recherche Entreprises → entreprises secteur santé (live API)
1045
+ * - FINESS (data.gouv) → établissements sanitaires et médico-sociaux (dump CSV bimestriel)
1046
+ * - Annuaire Santé Ameli (data.gouv/CNAM) → PS libéraux conventionnés (dump CSV hebdo)
1047
+ * - RPPS / Annuaire Santé ANS (data.gouv) → tous les PS (libéraux + salariés), ID stable, dump CSV mensuel
1048
+ * - FHIR ANS live → fallback fraîcheur quotidienne pour lookup individuel par RPPS ID
1049
+ */
1050
+
1051
+ declare const SANTE_VERSION = "0.5.1";
1052
+
1053
+ export { type AnsFhirPractitioner, type ByCategorieInput, type Dirigeant, type Entreprise, type Etablissement, type EtablissementFiness, FINESS_CATEGORIES, FINESS_EHPAD, FINESS_FAMILY_CODES, FINESS_HOPITAUX, FINESS_LABOS, FINESS_MSP_CPTS, FINESS_PHARMACIES, type FilterAnnuaireOptions, type Finance, type FinessCategorieCode, type FinessFamille, type FinessFamilleQuery, type FinessQueryResult, type FinessResult, type InRadiusInput, type LoadFinessOptions, NAF_EHPAD, NAF_LABOS, NAF_MEDECINE_VILLE, NAF_PHARMACIES, NAF_SANTE, type NafCodeSante, type ProfessionnelSante, RPPS_CGU_NOTICE, RPPS_MODE_EXERCICE, type RppsDansEtablissementInput, type RppsInRadiusInput, type RppsLookupResult, type RppsParSpecialiteDeptInput, type RppsQueryResult, type RppsResult, SANTE_VERSION, type SearchEntreprisesOptions, type SearchEntreprisesResult, type SearchFinessOptions, type StreamAnnuaireOptions, ensureAnnuaireAmeli, finessFamille, getAnsFhirApiKey, getAnsFhirBaseUrl, getEntrepriseBySiren, getFinessByCategorie, getFinessByNumFiness, getFinessInRadius, getInseeApiKey, getRppsById, getRppsDansEtablissement, getRppsInRadius, getRppsParSpecialiteDept, haversineDistance, libelleCategorieFiness, libelleNaf, loadFiness, loadProfessionnels, lookupPractitionerByRpps, lookupSirenViaInsee, searchEntreprises, searchEtablissementsFiness, streamProfessionnels };