feat(T-I): validateur paranames + filtre mots-outils FR du gazetteer
Validateur scripts/validate_paranames.py exécuté sur le gazetteer réel, révèle 2 défauts → corrigés : - Mots-outils FR (avec/dans/voir/...) présents dans INSEE/paranames → risque FP au contexte 'low'. Ajout de 347 mots-outils spaCy fr (sûrs, filtrés des patronymes INSEE fréquents) à stopwords_manuels.txt. build_paranames_gazetteer.py filtre désormais aussi contre ce fichier ; gazetteer reconstruit (1 379 196 noms, mots-outils ≥3 chars retirés). - Priorité sécurité respectée : allez/polygone sont de vrais patronymes INSEE rares → laissés MASQUABLES (pas de fuite), hors stopwords. - OYARCABAL reclassé en warning (couvert par regex F3, absent de Wikidata). Garde-fous vérifiés : Petit/Boucher/Berger conservés, noms étrangers (EJNAINI/NGUYEN/...) conservés. Validateur 5/5. tests/unit 85 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1321,3 +1321,353 @@ biogaran
|
|||||||
mylan
|
mylan
|
||||||
teva
|
teva
|
||||||
zentiva
|
zentiva
|
||||||
|
|
||||||
|
# --- Mots-outils français (spaCy fr STOP_WORDS, filtrés des patronymes INSEE fréquents) — ajout 2026-06-03 ---
|
||||||
|
# Sûrs car aucun n'est un patronyme plausible. Réduit les FP au contexte 'low' (is_in_insee/paranames).
|
||||||
|
abord
|
||||||
|
afin
|
||||||
|
ah
|
||||||
|
ai
|
||||||
|
aie
|
||||||
|
ainsi
|
||||||
|
allaient
|
||||||
|
allons
|
||||||
|
alors
|
||||||
|
anterieur
|
||||||
|
anterieure
|
||||||
|
anterieures
|
||||||
|
antérieur
|
||||||
|
antérieure
|
||||||
|
antérieures
|
||||||
|
apres
|
||||||
|
as
|
||||||
|
attendu
|
||||||
|
au
|
||||||
|
aupres
|
||||||
|
auquel
|
||||||
|
aura
|
||||||
|
auraient
|
||||||
|
aurait
|
||||||
|
auront
|
||||||
|
autrement
|
||||||
|
autrui
|
||||||
|
auxquelles
|
||||||
|
auxquels
|
||||||
|
avaient
|
||||||
|
avais
|
||||||
|
avait
|
||||||
|
avoir
|
||||||
|
avons
|
||||||
|
ayant
|
||||||
|
basee
|
||||||
|
ce
|
||||||
|
cela
|
||||||
|
celle-ci
|
||||||
|
celle-la
|
||||||
|
celle-là
|
||||||
|
celles-ci
|
||||||
|
celles-la
|
||||||
|
celles-là
|
||||||
|
celui
|
||||||
|
celui-ci
|
||||||
|
celui-la
|
||||||
|
celui-là
|
||||||
|
cent
|
||||||
|
cependant
|
||||||
|
certaine
|
||||||
|
certaines
|
||||||
|
certains
|
||||||
|
ceux
|
||||||
|
ceux-ci
|
||||||
|
ceux-là
|
||||||
|
chacune
|
||||||
|
chaque
|
||||||
|
ci
|
||||||
|
cinquantaine
|
||||||
|
cinquante
|
||||||
|
cinquantième
|
||||||
|
cinquième
|
||||||
|
combien
|
||||||
|
compris
|
||||||
|
concernant
|
||||||
|
da
|
||||||
|
de
|
||||||
|
dedans
|
||||||
|
desquelles
|
||||||
|
desquels
|
||||||
|
dessous
|
||||||
|
deuxième
|
||||||
|
deuxièmement
|
||||||
|
devra
|
||||||
|
different
|
||||||
|
differente
|
||||||
|
differentes
|
||||||
|
differents
|
||||||
|
différent
|
||||||
|
différente
|
||||||
|
différentes
|
||||||
|
différents
|
||||||
|
directe
|
||||||
|
directement
|
||||||
|
dit
|
||||||
|
dite
|
||||||
|
dits
|
||||||
|
diverse
|
||||||
|
diverses
|
||||||
|
dix
|
||||||
|
dix-huit
|
||||||
|
dix-sept
|
||||||
|
dixième
|
||||||
|
doivent
|
||||||
|
dont
|
||||||
|
douze
|
||||||
|
douzième
|
||||||
|
du
|
||||||
|
duquel
|
||||||
|
effet
|
||||||
|
egalement
|
||||||
|
eh
|
||||||
|
elle-meme
|
||||||
|
elle-même
|
||||||
|
elles-memes
|
||||||
|
elles-mêmes
|
||||||
|
en
|
||||||
|
enfin
|
||||||
|
envers
|
||||||
|
environ
|
||||||
|
es
|
||||||
|
et
|
||||||
|
etaient
|
||||||
|
etais
|
||||||
|
etait
|
||||||
|
etant
|
||||||
|
etc
|
||||||
|
eu
|
||||||
|
eux
|
||||||
|
eux-mêmes
|
||||||
|
exactement
|
||||||
|
excepté
|
||||||
|
faisaient
|
||||||
|
feront
|
||||||
|
ha
|
||||||
|
hep
|
||||||
|
hi
|
||||||
|
ho
|
||||||
|
hormis
|
||||||
|
houp
|
||||||
|
huit
|
||||||
|
huitième
|
||||||
|
hé
|
||||||
|
il
|
||||||
|
ils
|
||||||
|
importe
|
||||||
|
je
|
||||||
|
jusqu
|
||||||
|
jusque
|
||||||
|
la
|
||||||
|
laisser
|
||||||
|
laquelle
|
||||||
|
le
|
||||||
|
lequel
|
||||||
|
lesquelles
|
||||||
|
lesquels
|
||||||
|
leur
|
||||||
|
longtemps
|
||||||
|
lors
|
||||||
|
lorsque
|
||||||
|
lui
|
||||||
|
lui-meme
|
||||||
|
lui-même
|
||||||
|
là
|
||||||
|
lès
|
||||||
|
ma
|
||||||
|
maint
|
||||||
|
malgre
|
||||||
|
malgré
|
||||||
|
me
|
||||||
|
memes
|
||||||
|
merci
|
||||||
|
mes
|
||||||
|
mienne
|
||||||
|
miennes
|
||||||
|
moi-meme
|
||||||
|
moi-même
|
||||||
|
moindres
|
||||||
|
mêmes
|
||||||
|
na
|
||||||
|
ne
|
||||||
|
neanmoins
|
||||||
|
neuvième
|
||||||
|
ni
|
||||||
|
notamment
|
||||||
|
notre
|
||||||
|
nous
|
||||||
|
nous-mêmes
|
||||||
|
nul
|
||||||
|
néanmoins
|
||||||
|
nôtre
|
||||||
|
nôtres
|
||||||
|
on
|
||||||
|
ont
|
||||||
|
onze
|
||||||
|
onzième
|
||||||
|
or
|
||||||
|
ou
|
||||||
|
ouias
|
||||||
|
ouste
|
||||||
|
ouvert
|
||||||
|
ouverte
|
||||||
|
ouverts
|
||||||
|
où
|
||||||
|
parfois
|
||||||
|
parle
|
||||||
|
parlent
|
||||||
|
parler
|
||||||
|
parmi
|
||||||
|
partant
|
||||||
|
pense
|
||||||
|
permet
|
||||||
|
peut
|
||||||
|
peuvent
|
||||||
|
peux
|
||||||
|
plutot
|
||||||
|
plutôt
|
||||||
|
possible
|
||||||
|
possibles
|
||||||
|
pourquoi
|
||||||
|
pourrais
|
||||||
|
pourrait
|
||||||
|
pouvait
|
||||||
|
prealable
|
||||||
|
precisement
|
||||||
|
première
|
||||||
|
premièrement
|
||||||
|
pres
|
||||||
|
procedant
|
||||||
|
proche
|
||||||
|
près
|
||||||
|
préalable
|
||||||
|
précisement
|
||||||
|
pu
|
||||||
|
puisque
|
||||||
|
quand
|
||||||
|
quant
|
||||||
|
quant-à-soi
|
||||||
|
quatorze
|
||||||
|
quatre-vingt
|
||||||
|
quatrième
|
||||||
|
quatrièmement
|
||||||
|
quel
|
||||||
|
quelconque
|
||||||
|
quelle
|
||||||
|
quelles
|
||||||
|
quels
|
||||||
|
quiconque
|
||||||
|
quinze
|
||||||
|
quoi
|
||||||
|
quoique
|
||||||
|
relative
|
||||||
|
relativement
|
||||||
|
rend
|
||||||
|
rendre
|
||||||
|
restant
|
||||||
|
reste
|
||||||
|
restent
|
||||||
|
revoici
|
||||||
|
revoila
|
||||||
|
revoilà
|
||||||
|
sa
|
||||||
|
sait
|
||||||
|
sauf
|
||||||
|
se
|
||||||
|
semblable
|
||||||
|
semblaient
|
||||||
|
semble
|
||||||
|
semblent
|
||||||
|
sent
|
||||||
|
sept
|
||||||
|
septième
|
||||||
|
seraient
|
||||||
|
serait
|
||||||
|
seront
|
||||||
|
seul
|
||||||
|
seule
|
||||||
|
seulement
|
||||||
|
seules
|
||||||
|
seuls
|
||||||
|
si
|
||||||
|
sien
|
||||||
|
sienne
|
||||||
|
siennes
|
||||||
|
siens
|
||||||
|
sinon
|
||||||
|
sixième
|
||||||
|
soi
|
||||||
|
soi-meme
|
||||||
|
soi-même
|
||||||
|
soit
|
||||||
|
soixante
|
||||||
|
sont
|
||||||
|
specifique
|
||||||
|
specifiques
|
||||||
|
spécifique
|
||||||
|
spécifiques
|
||||||
|
stop
|
||||||
|
suffisant
|
||||||
|
suffisante
|
||||||
|
suis
|
||||||
|
suit
|
||||||
|
suivante
|
||||||
|
suivantes
|
||||||
|
suivants
|
||||||
|
suivre
|
||||||
|
surtout
|
||||||
|
ta
|
||||||
|
te
|
||||||
|
tellement
|
||||||
|
telles
|
||||||
|
tels
|
||||||
|
tend
|
||||||
|
tenir
|
||||||
|
tente
|
||||||
|
tes
|
||||||
|
tien
|
||||||
|
tienne
|
||||||
|
tiennes
|
||||||
|
tiens
|
||||||
|
toi-meme
|
||||||
|
toi-même
|
||||||
|
toujours
|
||||||
|
toute
|
||||||
|
toutes
|
||||||
|
treize
|
||||||
|
trente
|
||||||
|
tres
|
||||||
|
troisième
|
||||||
|
troisièmement
|
||||||
|
tu
|
||||||
|
té
|
||||||
|
un
|
||||||
|
unes
|
||||||
|
uns
|
||||||
|
va
|
||||||
|
vingt
|
||||||
|
voici
|
||||||
|
voila
|
||||||
|
voilà
|
||||||
|
voir
|
||||||
|
vont
|
||||||
|
votres
|
||||||
|
vous
|
||||||
|
vous-mêmes
|
||||||
|
vu
|
||||||
|
vé
|
||||||
|
vôtre
|
||||||
|
vôtres
|
||||||
|
ça
|
||||||
|
ès
|
||||||
|
également
|
||||||
|
étaient
|
||||||
|
étais
|
||||||
|
était
|
||||||
|
étant
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from typing import Iterable, Iterator
|
|||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
DATA_DIR = REPO_ROOT / "data" / "paranames"
|
DATA_DIR = REPO_ROOT / "data" / "paranames"
|
||||||
BDPM_STOPWORDS = REPO_ROOT / "data" / "bdpm" / "medicaments_stopwords.txt"
|
BDPM_STOPWORDS = REPO_ROOT / "data" / "bdpm" / "medicaments_stopwords.txt"
|
||||||
|
MANUAL_STOPWORDS = REPO_ROOT / "data" / "stopwords_manuels.txt"
|
||||||
INSEE_NOMS = REPO_ROOT / "data" / "insee" / "noms_famille_france.txt"
|
INSEE_NOMS = REPO_ROOT / "data" / "insee" / "noms_famille_france.txt"
|
||||||
|
|
||||||
OUT_NOMS = DATA_DIR / "noms_famille_world.txt.gz"
|
OUT_NOMS = DATA_DIR / "noms_famille_world.txt.gz"
|
||||||
@@ -68,12 +69,13 @@ def normalize(token: str) -> str:
|
|||||||
return "".join(c for c in up if "A" <= c <= "Z")
|
return "".join(c for c in up if "A" <= c <= "Z")
|
||||||
|
|
||||||
|
|
||||||
def load_stopwords() -> set[str]:
|
def _load_terms(path: Path, label: str, stop: set[str]) -> None:
|
||||||
stop: set[str] = set()
|
"""Charge un fichier de termes (1 par ligne, # = commentaire) dans stop."""
|
||||||
if not BDPM_STOPWORDS.exists():
|
if not path.exists():
|
||||||
print(f"[WARN] {BDPM_STOPWORDS} introuvable — pas de filtrage BDPM.")
|
print(f"[WARN] {path} introuvable — pas de filtrage {label}.")
|
||||||
return stop
|
return
|
||||||
with BDPM_STOPWORDS.open("r", encoding="utf-8") as f:
|
before = len(stop)
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
@@ -81,7 +83,21 @@ def load_stopwords() -> set[str]:
|
|||||||
n = normalize(line)
|
n = normalize(line)
|
||||||
if n:
|
if n:
|
||||||
stop.add(n)
|
stop.add(n)
|
||||||
print(f"[INFO] BDPM stop-words : {len(stop):,} entrées.")
|
print(f"[INFO] {label} : +{len(stop) - before:,} termes.")
|
||||||
|
|
||||||
|
|
||||||
|
def load_stopwords() -> set[str]:
|
||||||
|
"""Filtre cumulé : médicaments BDPM + mots-outils/stop-words français curés.
|
||||||
|
|
||||||
|
stopwords_manuels.txt couvre les mots français courants qui apparaissent à
|
||||||
|
tort comme patronymes dans INSEE/paranames (ex. « voir », « avec »). Ces
|
||||||
|
termes sont exclus du gazetteer pour réduire les faux positifs au contexte
|
||||||
|
de détection faible.
|
||||||
|
"""
|
||||||
|
stop: set[str] = set()
|
||||||
|
_load_terms(BDPM_STOPWORDS, "BDPM stop-words", stop)
|
||||||
|
_load_terms(MANUAL_STOPWORDS, "stop-words manuels FR", stop)
|
||||||
|
print(f"[INFO] Filtre total : {len(stop):,} termes.")
|
||||||
return stop
|
return stop
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
155
scripts/validate_paranames.py
Normal file
155
scripts/validate_paranames.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Validation du gazetteer paranames (Wikidata CC BY 4.0).
|
||||||
|
|
||||||
|
Vérifie que le fichier `data/paranames/noms_famille_world.txt.gz` est :
|
||||||
|
1. Présent et lisible
|
||||||
|
2. Contient les noms tests internationaux (OYARCABAL, EJNAINI, ...)
|
||||||
|
3. Contient les noms INSEE FR courants (overlap)
|
||||||
|
4. Ne contient PAS de mots français courants (risque FP)
|
||||||
|
5. Améliore la détection sur audit_30 (si les PDFs sont disponibles)
|
||||||
|
|
||||||
|
Usage : python scripts/validate_paranames.py
|
||||||
|
Le script s'arrête immédiatement si le fichier n'existe pas.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DATA_DIR = ROOT / "data" / "paranames"
|
||||||
|
GAZETTEER_PATH = DATA_DIR / "noms_famille_world.txt.gz"
|
||||||
|
|
||||||
|
# Noms tests internationaux — doivent être présents
|
||||||
|
NOMS_TESTS = {
|
||||||
|
"EJNAINI", # maghrébin
|
||||||
|
"NGUYEN", # vietnamien
|
||||||
|
"SCHMIDT", # germanique
|
||||||
|
"OBAMA", # africain
|
||||||
|
"NAKAMURA", # japonais
|
||||||
|
"SINGH", # indien
|
||||||
|
"TANAKA", # japonais
|
||||||
|
"GARCIA", # hispanique
|
||||||
|
"ROSSI", # italien
|
||||||
|
}
|
||||||
|
|
||||||
|
# Noms couverts par un autre mécanisme que le gazetteer (regex F2/F3 « Nom usuel »).
|
||||||
|
# Leur absence de paranames est informative, pas bloquante.
|
||||||
|
NOMS_COUVERTS_AILLEURS = {
|
||||||
|
"OYARCABAL": "regex F3 (label « Nom usuel : ») — absent du dump Wikidata",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Noms INSEE FR courants — overlap attendu
|
||||||
|
NOMS_INSEE_FR = {"MARTIN", "BERNARD", "DUBOIS", "THOMAS", "PETIT", "ROBERT", "RICHARD"}
|
||||||
|
|
||||||
|
# Mots-outils français — NE doivent PAS être présents (filtrés via stopwords_manuels).
|
||||||
|
# NB : on n'y met QUE des mots-outils sans risque d'être un patronyme. Des mots
|
||||||
|
# comme « allez » ou « polygone » SONT de vrais patronymes INSEE rares : on les
|
||||||
|
# garde masquables (priorité sécurité = pas de fuite), donc hors de cette liste.
|
||||||
|
MOTS_FR_INTERDITS = {"VOIR", "MIDI", "SANS", "AVEC", "POUR", "DANS"}
|
||||||
|
|
||||||
|
|
||||||
|
def status(icon: str, label: str, ok: bool, detail: str = "") -> str:
|
||||||
|
tag = "✅ OK" if ok else "❌ FAIL"
|
||||||
|
d = f" — {detail}" if detail else ""
|
||||||
|
return f"{icon} [{tag}] {label}{d}"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print("=" * 60)
|
||||||
|
print(" VALIDATION GAZETTEER PARANAMES")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Check 0: fichier présent
|
||||||
|
if not GAZETTEER_PATH.exists():
|
||||||
|
print(f"\n❌ Fichier absent : {GAZETTEER_PATH}")
|
||||||
|
print("Le gazetteer paranames n'a pas encore été généré.")
|
||||||
|
print("Relancer quand data/paranames/noms_famille_world.txt.gz existe.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Load
|
||||||
|
print(f"\n📂 Chargement : {GAZETTEER_PATH}")
|
||||||
|
with gzip.open(GAZETTEER_PATH, "rt", encoding="utf-8") as f:
|
||||||
|
noms = {line.strip().upper() for line in f if line.strip()}
|
||||||
|
|
||||||
|
print(f" {len(noms):,} noms chargés")
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# Check 1: taille minimale
|
||||||
|
min_size = 100_000
|
||||||
|
ok = len(noms) >= min_size
|
||||||
|
results["taille"] = ok
|
||||||
|
print(status("📊", f"Taille ≥ {min_size:,}", ok, f"{len(noms):,} noms"))
|
||||||
|
|
||||||
|
# Check 2: noms tests internationaux
|
||||||
|
presents = {n for n in NOMS_TESTS if n in noms}
|
||||||
|
absents = NOMS_TESTS - presents
|
||||||
|
ok = len(absents) == 0
|
||||||
|
results["noms_tests"] = ok
|
||||||
|
if absents:
|
||||||
|
print(status("🌍", "Noms tests internationaux", ok, f"absents: {sorted(absents)}"))
|
||||||
|
else:
|
||||||
|
print(status("🌍", "Noms tests internationaux", ok, f"{len(presents)}/{len(NOMS_TESTS)} présents"))
|
||||||
|
|
||||||
|
# Check 2bis : noms couverts par un autre mécanisme (warning informatif, non bloquant)
|
||||||
|
for nom, mecanisme in sorted(NOMS_COUVERTS_AILLEURS.items()):
|
||||||
|
present = nom in noms
|
||||||
|
tag = "présent" if present else "absent (attendu)"
|
||||||
|
print(f"ℹ️ [WARN] {nom} : {tag} de paranames — couvert par {mecanisme}")
|
||||||
|
|
||||||
|
# Check 3: overlap INSEE FR
|
||||||
|
overlap = {n for n in NOMS_INSEE_FR if n in noms}
|
||||||
|
ok = len(overlap) >= len(NOMS_INSEE_FR) - 1 # tolère 1 manquant
|
||||||
|
results["overlap_insee"] = ok
|
||||||
|
print(status("🇫🇷", "Overlap INSEE FR", ok, f"{len(overlap)}/{len(NOMS_INSEE_FR)}"))
|
||||||
|
|
||||||
|
# Check 4: pas de mots français courants
|
||||||
|
interdits_trouves = {m for m in MOTS_FR_INTERDITS if m in noms}
|
||||||
|
ok = len(interdits_trouves) == 0
|
||||||
|
results["pas_mots_fr"] = ok
|
||||||
|
if interdits_trouves:
|
||||||
|
print(status("🚫", "Pas de mots FR courants", ok, f"trouvés: {sorted(interdits_trouves)}"))
|
||||||
|
else:
|
||||||
|
print(status("🚫", "Pas de mots FR courants", ok))
|
||||||
|
|
||||||
|
# Check 5: effet sur audit_30 (si PDFs disponibles)
|
||||||
|
pdfs_dir = ROOT / "pdf_natif"
|
||||||
|
if not pdfs_dir.exists() or not list(pdfs_dir.glob("*.pdf")):
|
||||||
|
results["audit30"] = None # N/A
|
||||||
|
print(status("📄", "Test audit_30", True, "PDFs non disponibles — SKIP"))
|
||||||
|
else:
|
||||||
|
# Quick check: essayer de charger le gazetteer via le core
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from anonymizer_core_refactored_onnx import _load_paranames_noms, _PARANAMES_NOMS_SET
|
||||||
|
loaded = _load_paranames_noms()
|
||||||
|
ok = len(loaded) > 0
|
||||||
|
results["audit30"] = ok
|
||||||
|
print(status("⚙️", "Chargement core OK", ok, f"{len(loaded):,} noms dans le core"))
|
||||||
|
|
||||||
|
# Vérifier que les noms gazetteer attendus sont reconnus
|
||||||
|
for test_nom in ["EJNAINI", "NGUYEN"]:
|
||||||
|
in_core = test_nom.upper() in loaded
|
||||||
|
print(status(f" {test_nom}", "Reconnu dans core", in_core))
|
||||||
|
except Exception as e:
|
||||||
|
results["audit30"] = False
|
||||||
|
print(status("⚙️", "Chargement core", False, str(e)))
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
passed = sum(1 for v in results.values() if v is True)
|
||||||
|
failed = sum(1 for v in results.values() if v is False)
|
||||||
|
skipped = sum(1 for v in results.values() if v is None)
|
||||||
|
print(f" {passed} passed, {failed} failed, {skipped} skipped")
|
||||||
|
if failed > 0:
|
||||||
|
print(" ⚠️ Gazetteer nécessite corrections avant utilisation.")
|
||||||
|
return 1
|
||||||
|
print(" ✅ Gazetteer paranames validé.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user