fix(redact): masquer tokens collés à ponctuation ("Douar,nécessitant")
Fuite détectée lors du QC batch 22 : le nom "Douar" était dans l'audit
(NOM page 6) mais restait visible dans le PDF redacted_vector. Cause :
dans get_text('words') le word était 'Douar,nécessitant' (virgule collée
sans espace). _search_whole_word faisait un == strict après strip des
ponctuations frontières, mais la virgule était au MILIEU — pas stripée.
→ aucun match → aucun rectangle → fuite.
Fix : passe 2 dans _search_whole_word avec regex word-boundary sur le
texte complet du word (pattern `(?<![A-Za-zÀ-ÿ])token(?![A-Za-zÀ-ÿ])`)
+ bbox proportionnelle au ratio chars matched / chars total du word.
Approximation exacte sur polices monospace, précision ±pixels sur
polices proportionnelles — couverte par le rectangle de redaction.
Validation bout-en-bout sur trackare-BA042686-23090597 : "Douar" masqué
(0 page résiduelle). QC strict retombe de 1 anomalie à 0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3929,15 +3929,42 @@ def _search_ocr_words(ocr_words: List[Tuple[str, float, float, float, float]], t
|
||||
def _search_whole_word(page, token: str) -> list:
|
||||
"""Cherche un token comme mot entier (pas substring) via get_text('words').
|
||||
Évite les faux positifs de page.search_for() qui fait du substring matching.
|
||||
Gère les noms composés (JEAN-PIERRE) qui peuvent être splittés par le PDF."""
|
||||
Gère les noms composés (JEAN-PIERRE) qui peuvent être splittés par le PDF.
|
||||
Gère aussi les tokens collés à de la ponctuation sans espace : "Douar,nécessitant"
|
||||
(passe 2 avec regex word-boundary + bbox proportionnelle)."""
|
||||
rects = []
|
||||
token_lower = token.lower().strip()
|
||||
words = page.get_text("words")
|
||||
# Passe 1 : comparaison stricte après strip des ponctuations frontières.
|
||||
# Couvre la majorité des cas normaux (mots bien séparés).
|
||||
for w in words:
|
||||
# w = (x0, y0, x1, y1, word, block_no, line_no, word_no)
|
||||
word_text = w[4].strip(".,;:!?()[]{}\"'«»-–—/\\")
|
||||
if word_text.lower() == token_lower:
|
||||
rects.append(fitz.Rect(w[0], w[1], w[2], w[3]))
|
||||
# Passe 2 : token collé à d'autres mots via ponctuation interne.
|
||||
# Ex: "Douar,nécessitant" où "Douar" doit être masqué. Le strip ci-dessus
|
||||
# ne marche pas (la virgule est au milieu). Utiliser regex word-boundary
|
||||
# sur le texte complet du word, calculer bbox proportionnelle.
|
||||
if not rects and len(token_lower) >= 3:
|
||||
pattern = re.compile(
|
||||
r"(?<![A-Za-zÀ-ÿ])" + re.escape(token_lower) + r"(?![A-Za-zÀ-ÿ])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
for w in words:
|
||||
word_text = w[4]
|
||||
if len(word_text) < len(token_lower):
|
||||
continue
|
||||
for m in pattern.finditer(word_text):
|
||||
# Bbox proportionnelle : approximation pour polices proportionnelles,
|
||||
# exacte pour chasses fixes. Marge de quelques pixels couverte par
|
||||
# le rectangle de redaction.
|
||||
wlen = len(word_text)
|
||||
start_ratio = m.start() / wlen
|
||||
end_ratio = m.end() / wlen
|
||||
x0 = w[0] + (w[2] - w[0]) * start_ratio
|
||||
x1 = w[0] + (w[2] - w[0]) * end_ratio
|
||||
rects.append(fitz.Rect(x0, w[1], x1, w[3]))
|
||||
# Fallback pour noms composés avec tiret (JEAN-PIERRE) splittés par le PDF
|
||||
if not rects and "-" in token:
|
||||
parts = [p for p in token.split("-") if p]
|
||||
|
||||
Reference in New Issue
Block a user