feat: réduction FP + gazetteers adresses FINESS + batch parallèle + corrections multi-axes

- Token min length relevé de 2-3 → 4 chars (élimine FP EPO, IRC, SIB...)
- Stop-words enrichis : acronymes médicaux 3 lettres, termes pharma, soins infirmiers
- BDPM stop-words : ~7300 noms commerciaux + DCI/substances actives
- Gazetteers adresses FINESS : 63K patterns Aho-Corasick (position-preserving normalization)
- Filtre contextuel anatomique pour FINESS établissements
- Nouvelles regex : RE_CIVILITE_COMMA_LIST, RE_EXTRACT_NOM_UTILISE, RE_EXTRACT_PRENOM,
  RE_NUM_EXAMEN_PATIENT, RE_ADRESSE_LIEU_DIT, RE_CIVILITE_INITIALE, Dr X.NOM
- URLs complètes (RE_URL) + détection multiline
- N° venue inversé (layout-aware) + EPISODE/NDA dans _CRITICAL_PII_TYPES
- HospitalFilter désactivé pour ADRESSE/TEL/VILLE/EPISODE (identifient le patient)
- Batch silver export parallélisé (multiprocessing spawn, N workers)
- Seuil sur-masquage relevé à 8%, server.py enrichi (source regex/ner)
- Blacklist villes : COURANT, PARIS ; contexte villes étendu (UHCD, spécialités)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 09:26:56 +01:00
parent a827d860f1
commit 49ff464e6e
18 changed files with 358579 additions and 232 deletions

View File

@@ -166,24 +166,13 @@ class HospitalFilter:
Returns:
True si la détection doit être filtrée (faux positif)
"""
# Filtrer par type
if pii_type == "ADRESSE":
return self.is_hospital_address(text)
elif pii_type == "CODE_POSTAL":
return self.is_hospital_postal_code(text)
elif pii_type == "VILLE":
return self.is_hospital_city(text)
elif pii_type == "TEL":
return self.is_hospital_phone(text)
elif pii_type == "EPISODE":
# Filtrer les épisodes qui proviennent du nom de fichier
# (répétés dans les en-têtes/pieds de page des documents trackare)
return self.is_episode_in_filename(text, filename)
# ADRESSE, CODE_POSTAL, VILLE, TEL : NE PAS filtrer.
# Les coordonnées hospitalières identifient indirectement le patient
# et doivent être masquées (validé par contrôle humain 2026-03-12).
# EPISODE : NE PAS filtrer.
# Les numéros d'épisode identifient le patient (validé 2026-03-14).
return False
def filter_detections(self, detections: List[Dict], filename: str = "", is_trackare: bool = False) -> List[Dict]:
@@ -222,15 +211,17 @@ if __name__ == "__main__":
# Tests
test_cases = [
("ADRESSE", "13, Avenue de l'Interne J", "", -1, True),
# ADRESSE, CODE_POSTAL, VILLE, TEL : ne sont plus filtrés (identifient le patient)
("ADRESSE", "13, Avenue de l'Interne J", "", -1, False),
("ADRESSE", "22 LOT MENDI ALDE", "", -1, False),
("CODE_POSTAL", "64109 BAYONNE CEDEX", "", -1, True),
("CODE_POSTAL", "64109 BAYONNE CEDEX", "", -1, False),
("CODE_POSTAL", "64130", "", -1, False),
("VILLE", "BAYONNE CEDEX", "", -1, True),
("VILLE", "BAYONNE CEDEX", "", -1, False),
("VILLE", "CHERAUTE", "", -1, False),
("VILLE", "DROIT", "", -1, True), # Terme anatomique
("TEL", "05 59 44 35 35", "", -1, True),
("VILLE", "DROIT", "", -1, False),
("TEL", "05 59 44 35 35", "", -1, False),
("TEL", "0676085336", "", -1, False),
# EPISODE : filtré uniquement si provient du nom de fichier trackare
("EPISODE", "23202435", "trackare-14004105-23202435", -1, True),
("EPISODE", "23102610", "CRH_23102610", 0, False),
]