fix(detect): accepter prénoms 3 chars après Dr/Mme (Ute, Eva, Léo…)

Audit manuel après batch QC : 20 occurrences de "Dr Ute" dans
trackare-03020576-23175616 non masquées. Audit jsonl confirme : 0 hit pour
"Ute" → pas détecté.

Cause : _add_candidate (deux implémentations, lignes 1908 et 2225) filtrait
len(token) < 4, empêchant la création du NameCandidate pour "Ute" (3 chars)
même avec bypass_stopwords=True. La cross-validation écrasait alors
all_names avec validated_names (vide pour Ute), et _apply_extracted_names
ne recevait donc jamais Ute.

Le commit 2f79f7c avait fait le fix uniquement dans _apply_extracted_names.
Fix incomplet : le filtre amont _add_candidate rejetait avant.

Correctif symétrique sur _add_candidate (×2) + _add_tokens_force_first :
accepter 3 chars UNIQUEMENT si bypass=True (contexte Dr/Mme) ET majuscule
initiale ET alpha pur. 2 chars reste filtré (initiales ambigues).

Validation :
- "DR. DURANTEAU Ute" matche bien RE_EXTRACT_DR_DEST et capture "DURANTEAU Ute"
- Audit produit "Ute DURANTEAU" en bloc + "DURANTEAU" seul (41 hits total)
- PDF redacted : 0 résiduel "Ute" (avant : 38)

Cas protégés :
- "Ute" accepté : bypass=True, U majuscule, alpha ✓
- "Les" refusé : bypass=True mais stopword (filtré ailleurs) ✓
- "JF" refusé : 2 chars, filtre longueur < 3 ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 17:21:54 +02:00
parent f17438c2ec
commit 8e43d8d1ae

View File

@@ -1903,9 +1903,13 @@ def _extract_trackare_identity(full_text: str) -> Tuple[set, List[PiiHit], set,
force_names: set = set() # noms issus de contextes structurés (DR., Signé, etc.) → bypass stop words force_names: set = set() # noms issus de contextes structurés (DR., Signé, etc.) → bypass stop words
def _add_candidate(token: str, source: str, strength: str, bypass: bool): def _add_candidate(token: str, source: str, strength: str, bypass: bool):
"""Ajoute un NameCandidate à la liste.""" """Ajoute un NameCandidate à la liste.
Accepte les prénoms courts 3 chars (Ute, Eva, Léo…) si bypass=True
(contexte Dr/Mme fort) ET majuscule initiale + alpha pur."""
token = token.strip(" .-'(),") token = token.strip(" .-'(),")
if len(token) < 4: if len(token) < 3:
return
if len(token) == 3 and not (bypass and token[0].isupper() and token.isalpha()):
return return
candidates.append(NameCandidate( candidates.append(NameCandidate(
token=token, source=source, token=token, source=source,
@@ -2220,9 +2224,13 @@ def _extract_document_names(full_text: str, cfg: Dict[str, Any]) -> Tuple[set, s
candidates: List[NameCandidate] = [] candidates: List[NameCandidate] = []
def _add_candidate(token: str, source: str, strength: str, bypass: bool): def _add_candidate(token: str, source: str, strength: str, bypass: bool):
"""Ajoute un NameCandidate à la liste (dédupliqué par token+source).""" """Ajoute un NameCandidate à la liste (dédupliqué par token+source).
Accepte les prénoms courts 3 chars (Ute, Eva, Léo…) si bypass=True
(contexte Dr/Mme fort) ET majuscule initiale + alpha pur."""
token = token.strip(" .-'") token = token.strip(" .-'")
if len(token) < 4: if len(token) < 3:
return
if len(token) == 3 and not (bypass and token[0].isupper() and token.isalpha()):
return return
candidates.append(NameCandidate( candidates.append(NameCandidate(
token=token, source=source, token=token, source=source,
@@ -2270,12 +2278,21 @@ def _extract_document_names(full_text: str, cfg: Dict[str, Any]) -> Tuple[set, s
Après Dr/Mme, tous les tokens sont des noms — même s'ils sont Après Dr/Mme, tous les tokens sont des noms — même s'ils sont
homonymes de termes médicaux (ex: Dr Laurence MASSE). homonymes de termes médicaux (ex: Dr Laurence MASSE).
Accepte les prénoms courts 3 chars (Dr Ute, Dr Eva, Dr Léo) : le
contexte Dr/Mme est suffisamment fort pour lever le filtre de
longueur, à condition que le token soit alpha et commence par
une majuscule. 2 chars reste filtré (trop ambigu : initiales).
""" """
_add_compound(match_str) _add_compound(match_str)
tokens = match_str.split() tokens = match_str.split()
for token in tokens: for token in tokens:
token = token.strip(" .-'") token = token.strip(" .-'")
if len(token) < 4: if len(token) < 3:
continue
# 3 chars : accepter uniquement si majuscule initiale + alpha
# (évite "Les", "Des" mais accepte "Ute", "Eva").
if len(token) == 3 and not (token[0].isupper() and token.isalpha()):
continue continue
if token.upper() in wl_sections or token in wl_phrases: if token.upper() in wl_sections or token in wl_phrases:
continue continue