fix(finess): inclure les entjur + supprimer code mort _FINESS_ETAB_NAMES

Deux corrections exploitant mieux les gazetteers FINESS/INSEE pour réduire la
dépendance au YAML force_mask_terms.

1. scripts/build_finess_gazetteers.py : ne lisait que col 1 (finess_et) du CSV.
   Les col 2 (entjur, entité juridique) étaient ignorés. ~48k numéros
   juridiques manqués, dont 640780417 (CHCB entjur) forcé en YAML à cause
   de cette lacune. Fix : lecture col 1 + col 2 avec déduplication.
   Régénération : 101 941 → 150 436 numéros (+48 495).

2. anonymizer_core_refactored_onnx.py :
   - _FINESS_ETAB_NAMES (122k noms) chargé mais jamais consulté après le
     refactoring NER-first (le matching passe par l'Aho-Corasick sur
     etablissements_distinctifs.txt). Suppression → -122k entrées RAM.
   - _INSEE_PRENOMS (lowercase) et _INSEE_PRENOMS_SET (uppercase sans accents)
     lisaient deux fois le même fichier prenoms_france.txt. Fusion en une
     seule passe disque, les deux formes dérivées en mémoire. -36k lectures.

Validation :
- 640780417 présent dans _FINESS_NUMBERS après rebuild
- 122 hits sur trackare-18007562 (non-régression)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 09:33:07 +02:00
parent 8e458c16ca
commit fd95ae5f2a
3 changed files with 48547 additions and 65 deletions

View File

@@ -115,24 +115,49 @@ def _load_bdpm_medication_names() -> set:
return set()
# ----------------- Gazetteers INSEE (prénoms + communes) -----------------
_INSEE_PRENOMS: set = set()
_INSEE_COMMUNES: set = set()
# ----------------- Gazetteers INSEE (prénoms + communes + noms de famille) -----------------
# Prénoms et noms de famille sont utilisés sous deux formes :
# - _INSEE_PRENOMS (lowercase) : check rapide "w.lower() in _INSEE_PRENOMS"
# - _INSEE_PRENOMS_SET (uppercase sans accents, normalisé NFKD) : cross-validation NER
# Une seule lecture fichier alimente les deux sets (avant : 2 passes disque pour
# le même fichier prenoms_france.txt, reliquat du refactoring NER-first).
_INSEE_PRENOMS: set = set() # lowercase
_INSEE_PRENOMS_SET: set = set() # uppercase sans accents
_INSEE_COMMUNES: set = set() # uppercase
_INSEE_NOMS_FAMILLE: set = set() # uppercase sans accents
def _normalize_nfkd_upper(s: str) -> str:
"""Supprime les accents et met en majuscules (pour matching INSEE)."""
import unicodedata
return "".join(
c for c in unicodedata.normalize("NFD", s)
if unicodedata.category(c) != "Mn"
).upper()
def _load_insee_gazetteers():
"""Charge les gazetteers INSEE (prénoms français + communes)."""
global _INSEE_PRENOMS, _INSEE_COMMUNES
"""Charge les gazetteers INSEE en une seule passe par fichier.
Alimente _INSEE_PRENOMS (lowercase) et _INSEE_PRENOMS_SET (uppercase sans accents)
depuis le même fichier prenoms_france.txt."""
global _INSEE_PRENOMS, _INSEE_PRENOMS_SET, _INSEE_COMMUNES, _INSEE_NOMS_FAMILLE
data_dir = Path(__file__).parent / "data" / "insee"
# Prénoms (lowercase, >= 3 chars)
# Prénoms : lecture unique, deux formes dérivées
prenoms_path = data_dir / "prenoms_france.txt"
if prenoms_path.exists():
try:
_INSEE_PRENOMS = {
line.strip().lower() for line in prenoms_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 3
}
log.info(f"Gazetteers INSEE prénoms: {len(_INSEE_PRENOMS)} entrées")
prenoms_lc = set()
prenoms_nfkd = set()
for line in prenoms_path.read_text(encoding="utf-8").splitlines():
raw = line.strip()
if raw and len(raw) >= 3:
prenoms_lc.add(raw.lower())
prenoms_nfkd.add(_normalize_nfkd_upper(raw))
_INSEE_PRENOMS = prenoms_lc
_INSEE_PRENOMS_SET = prenoms_nfkd
log.info(f"Gazetteers INSEE prénoms: {len(_INSEE_PRENOMS)} entrées "
f"(lowercase + uppercase-nfkd)")
except Exception as e:
log.warning(f"Erreur chargement prénoms INSEE: {e}")
@@ -148,26 +173,7 @@ def _load_insee_gazetteers():
except Exception as e:
log.warning(f"Erreur chargement communes INSEE: {e}")
_load_insee_gazetteers()
# ----------------- Gazetteers INSEE noms de famille + prénoms (uppercase, sans accents) ---------
_INSEE_NOMS_FAMILLE: set = set()
_INSEE_PRENOMS_SET: set = set()
def _normalize_nfkd_upper(s: str) -> str:
"""Supprime les accents et met en majuscules (pour matching INSEE)."""
import unicodedata
return "".join(
c for c in unicodedata.normalize("NFD", s)
if unicodedata.category(c) != "Mn"
).upper()
def _load_insee_noms_prenoms():
"""Charge noms de famille et prénoms INSEE, normalisés uppercase sans accents."""
global _INSEE_NOMS_FAMILLE, _INSEE_PRENOMS_SET
data_dir = Path(__file__).parent / "data" / "insee"
# Noms de famille (uppercase sans accents)
noms_path = data_dir / "noms_famille_france.txt"
if noms_path.exists():
try:
@@ -180,24 +186,12 @@ def _load_insee_noms_prenoms():
except Exception as e:
log.warning(f"Erreur chargement noms de famille INSEE: {e}")
prenoms_path = data_dir / "prenoms_france.txt"
if prenoms_path.exists():
try:
_INSEE_PRENOMS_SET = {
_normalize_nfkd_upper(line.strip())
for line in prenoms_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 3
}
log.info(f"Gazetteers INSEE prénoms (set): {len(_INSEE_PRENOMS_SET)} entrées")
except Exception as e:
log.warning(f"Erreur chargement prénoms INSEE (set): {e}")
_load_insee_noms_prenoms()
_load_insee_gazetteers()
# ----------------- Gazetteer FINESS (établissements de santé) -----------------
_FINESS_NUMBERS: set = set() # numéros FINESS 9 chiffres
_FINESS_ETAB_NAMES: set = set() # noms d'établissements (lowercase)
_FINESS_NUMBERS: set = set() # numéros FINESS 9 chiffres (structure + entjur)
_FINESS_TELEPHONES: set = set() # téléphones 10 chiffres
_FINESS_VILLES: set = set() # villes FINESS (uppercase)
_FINESS_AC = None # Automate Aho-Corasick pour noms distinctifs
@@ -281,8 +275,8 @@ def _normalize_for_matching(s: str) -> str:
def _load_finess_gazetteers():
"""Charge les gazetteers FINESS (établissements, numéros, téléphones, villes, Aho-Corasick)."""
global _FINESS_NUMBERS, _FINESS_ETAB_NAMES, _FINESS_TELEPHONES, _FINESS_VILLES, _FINESS_AC
"""Charge les gazetteers FINESS (numéros, téléphones, villes, Aho-Corasick)."""
global _FINESS_NUMBERS, _FINESS_TELEPHONES, _FINESS_VILLES, _FINESS_AC
data_dir = Path(__file__).parent / "data" / "finess"
# Numéros FINESS
@@ -297,20 +291,9 @@ def _load_finess_gazetteers():
except Exception as e:
log.warning(f"Erreur chargement FINESS numéros: {e}")
# Noms d'établissements complets (pour debug/référence)
noms_path = data_dir / "etablissements_noms.txt"
if noms_path.exists():
try:
_FINESS_ETAB_NAMES = {
line.strip().lower() for line in noms_path.read_text(encoding="utf-8").splitlines()
if line.strip() and len(line.strip()) >= 6
}
log.info(f"Gazetteer FINESS noms: {len(_FINESS_ETAB_NAMES)} entrées")
except Exception as e:
log.warning(f"Erreur chargement FINESS noms: {e}")
# Noms distinctifs : chargement différé (Aho-Corasick construit au premier appel,
# car _MEDICAL_STOP_WORDS_SET n'est pas encore défini à ce stade du module)
# etablissements_noms.txt volontairement PAS chargé — utilisé uniquement pour
# debug/inspection. Le matching des noms passe par l'Aho-Corasick construit
# sur etablissements_distinctifs.txt (chargement différé).
# Villes FINESS
villes_path = data_dir / "villes_finess.txt"

File diff suppressed because it is too large Load Diff

View File

@@ -109,10 +109,14 @@ def main():
if len(row) < 16:
continue
# Numéro FINESS (col 1)
finess = row[1].strip()
if re.match(r"^\d{9}$", finess):
finess_numbers.add(finess)
# Numéros FINESS : col 1 = finess_et (structure), col 2 = entjur (entité juridique).
# Les deux sont des identifiants 9 chiffres réels du référentiel FINESS et doivent
# être masqués. Avant ce fix, seul finess_et était extrait (~102k), et les ~48k
# entjur étaient manqués — provoquant des fuites (ex: 640780417 entjur CHCB).
for col_idx in (1, 2):
finess = row[col_idx].strip() if col_idx < len(row) else ""
if re.match(r"^\d{9}$", finess):
finess_numbers.add(finess)
# Noms (col 3 = court, col 4 = long)
for col_idx in (3, 4):