Votre base CRM compte 50 000 enregistrements. Certains ont été importés depuis un fichier Excel, d'autres saisis manuellement par des commerciaux, d'autres encore migrés depuis un ancien système. Résultat : "av." et "avenue" cohabitent pour la même voie, des codes postaux ont perdu le premier 0 ou ont pris une notation scientifique, des communes ont fusionné depuis la saisie initiale. Comparer ces adresses brutes entre elles ne donne rien, les doublons passent à travers, le géocodage échoue.
Le problème n'est pas de valider des adresses à la saisie. C'est de traiter une base qui existe déjà. C'est un problème d'ETL, pas de formulaire. La différence entre le préventif et le curatif. La qualité de donnée s'applique aussi sur le stock.
Pourquoi la déduplication et le géocodage échouent sur des adresses brutes
Une adresse non normalisée est une chaîne de caractères libre. Deux enregistrements pour le même lieu peuvent s'écrire de dizaines de façons différentes :
10 avenue du Général de Gaulle
10 av gal de gaulle
10 Av. Général DeGaulle
10, avenue du général-de-gaulle
Aucun algorithme de comparaison de chaînes, même avec une distance de Levenshtein, ne rapprochera ces quatre variantes de manière fiable à l'échelle. Sans oublier que parfois les différences peuvent être plus importantes. Général de Gaulle ou Maréchal de Gaulle ? La seule approche robuste : passer chaque adresse par un référentiel officiel pour obtenir une forme canonique identique, puis comparer les formes canoniques.
C'est exactement ce que fait la vérification d'adresse via la Base Adresse Nationale. Elle vous aide à rendre les choses comparables. Il n'est pas forcément nécessaire de normaliser les adresses dans les règles de l'art pour faire une déduplication, les adresses de la BAN sont suffisantes pour rendre les informations homogènes donc comparables.
Architecture du pipeline
Un pipeline de nettoyage d'adresses suit systématiquement la même logique :
/v1/address/verifyLe score retourné par l'API est la clé de la décision automatique. Au-dessus de 0,8, la correspondance est au numéro dans la voie : la mise à jour est fiable. Entre 0,5 et 0,8, la voie est reconnue mais le numéro est incertain — une revue manuelle est préférable. En dessous de 0,5, l'adresse est trop dégradée pour être corrigée automatiquement.
Implémentation Python
Structure de base
import requests
import pandas as pd
from dataclasses import dataclass, field
from typing import Optional
API_KEY = "votre_clé_api"
BASE_URL = "https://api.trustydata.fr/v1/address/verify"
@dataclass
class AddressResult:
raw: str
normalized: Optional[str] = None
score: float = 0.0
lat: Optional[float] = None
lon: Optional[float] = None
code_insee: Optional[str] = None
iris: Optional[str] = None
status: str = "pending" # "ok", "review", "rejected", "error"
def verify_address(raw: str) -> AddressResult:
result = AddressResult(raw=raw)
try:
r = requests.get(
BASE_URL,
params={"q": raw, "limit": 1},
headers={"X-API-Key": API_KEY},
timeout=5,
)
r.raise_for_status()
data = r.json()
if data["status"] == "OK" and data["choices"]:
choice = data["choices"][0]
result.normalized = choice["adresse"]
result.score = choice["score"]
result.lat = choice["position"]["lat"]
result.lon = choice["position"]["lon"]
result.code_insee = choice.get("code_insee")
result.iris = choice.get("geocoding", {}).get("code_iris")
if result.score > 0.8:
result.status = "ok"
elif result.score >= 0.5:
result.status = "review"
else:
result.status = "rejected"
else:
result.status = "rejected"
except requests.RequestException as e:
result.status = "error"
return result
Traitement d'un DataFrame avec parallélisation
Pour une base de plusieurs milliers de lignes, la parallélisation réduit le temps de traitement proportionnellement au nombre de workers. Avec 10 workers et une API qui répond en ~100 ms, vous traitez ~100 adresses/seconde.
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm # pip install tqdm
def process_dataframe(
df: pd.DataFrame,
address_col: str,
max_workers: int = 10,
) -> pd.DataFrame:
"""
Vérifie et normalise toutes les adresses d'un DataFrame.
Ajoute les colonnes : adresse_normalisee, score, lat, lon,
code_insee, code_iris, statut_verification.
"""
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(verify_address, row[address_col]): idx
for idx, row in df.iterrows()
}
for future in tqdm(as_completed(futures), total=len(futures)):
idx = futures[future]
results[idx] = future.result()
df["adresse_normalisee"] = [results[i].normalized for i in df.index]
df["score_verification"] = [results[i].score for i in df.index]
df["lat"] = [results[i].lat for i in df.index]
df["lon"] = [results[i].lon for i in df.index]
df["code_insee"] = [results[i].code_insee for i in df.index]
df["code_iris"] = [results[i].iris for i in df.index]
df["statut_verification"] = [results[i].status for i in df.index]
return df
Exploitation des résultats
# Chargement
df = pd.read_csv("clients.csv")
# Préparation : concaténer les champs si adresse éclatée en colonnes
df["adresse_complete"] = (
df["numero"].fillna("") + " "
+ df["voie"].fillna("") + " "
+ df["code_postal"].fillna("") + " "
+ df["commune"].fillna("")
).str.strip()
# Traitement
df = process_dataframe(df, address_col="adresse_complete")
# Tri par statut
ok = df[df["statut_verification"] == "ok"]
to_review = df[df["statut_verification"] == "review"]
rejected = df[df["statut_verification"] == "rejected"]
print(f"Mise à jour automatique : {len(ok)} ({len(ok)/len(df)*100:.1f}%)")
print(f"À revoir manuellement : {len(to_review)} ({len(to_review)/len(df)*100:.1f}%)")
print(f"Non résolus : {len(rejected)} ({len(rejected)/len(df)*100:.1f}%)")
# Export
ok.to_csv("clients_valides.csv", index=False)
to_review.to_csv("clients_a_revoir.csv", index=False)
rejected.to_csv("clients_rejetes.csv", index=False)
Dédupliquer après normalisation
Une fois les adresses normalisées, la déduplication devient fiable. L'identifiant BAN (id_ban) retourné dans la réponse est le plus robuste — deux adresses identiques dans le référentiel ont le même id_ban, quelles que soient leurs variantes originales.
# Déduplication sur l'identifiant BAN
# (nécessite d'avoir stocké l'id_ban dans le résultat)
doublons = df[df["statut_verification"] == "ok"].duplicated(
subset=["adresse_normalisee"], keep="first"
)
print(f"Doublons détectés après normalisation : {doublons.sum()}")
df_dedup = df[~doublons]
À défaut de l'id_ban, l'adresse normalisée elle-même fait un bon proxy de déduplication — elle est produite par le même référentiel pour tous les enregistrements.
Enrichissement automatique en sortie
Ce pipeline ne fait pas que corriger les adresses. Chaque enregistrement résolu ressort enrichi avec :
- Coordonnées GPS (WGS84) : utilisables directement pour l'affichage cartographique ou le calcul de zone de chalandise
- Code INSEE commune : pour les jointures avec des référentiels INSEE, les agrégations par territoire
- Code IRIS : le découpage statistique infra-communal de l'INSEE, utile pour croiser avec les données socio-démographiques Filosofi
- Code Carreau : le découpage statistique de 200 m, le maillage le plus précis pour vos statistiques.
C'est le principal avantage d'une API qui s'appuie sur la BAN et l'INSEE simultanément : la correction de l'adresse et l'enrichissement géographique sont produits dans le même appel.
Points d'attention
Concaténer les champs avant l'appel. Si votre base stocke le numéro, la voie, le code postal et la commune dans des colonnes séparées, concaténez-les en une seule chaîne avant l'appel API. L'algorithme de matching de la BAN est plus efficace sur une chaîne libre que sur des champs structurés fournis séparément.
Gérer les adresses CEDEX. Les codes CEDEX ne figurent pas dans la BAN. Si votre base contient des adresses professionnelles avec CEDEX, filtrez-les en amont ou traitez-les séparément — elles reviendront systématiquement avec un score bas.
Calibrer les seuils sur vos données. Les seuils 0,8 / 0,5 sont des points de départ. Sur une base de prospection avec beaucoup d'adresses incomplètes, vous pouvez abaisser le seuil de mise à jour automatique à 0,75. Sur un dossier de souscription, remontez le seuil à 0,9. Faites tourner le pipeline sur un échantillon de 500 lignes, inspectez les cas limites, puis calibrez.
Revalider périodiquement. La BAN est mise à jour en continu. Des communes fusionnent, des voies sont renommées. Une adresse validée en 2022 peut retourner un score dégradé aujourd'hui. Intégrez une date de dernière vérification dans votre modèle de données et revalidez les enregistrements anciens.
Ce que ce pipeline ne remplace pas
Ce pipeline produit une validation géographique basée sur la Base Adresse Nationale — il vérifie qu'une adresse existe et retourne sa forme normalisée avec coordonnées GPS. Il ne produit pas de certification RNVP au sens de la norme Afnor NF Z 10-011, qui est une opération distincte requise pour les campagnes de mailing postal avec routage certifié La Poste. Si votre objectif final est un fichier routé, ce pipeline est une étape de préparation utile, mais il devra être complété par un prestataire agréé SNA.
Pour aller plus loin
L'API retourne également les données du carroyage INSEE à 200 mètres pour chaque adresse résolue (plan Business) : revenus médians, structure des ménages, parc de logements. Dans un pipeline CRM, cela permet de scorer chaque client selon le profil socio-démographique de son adresse — sans aucune donnée personnelle supplémentaire à collecter.
➜ Découvrir le use case vérification d'adresses
➜ Enrichissement statistique IRIS et carroyage INSEE
TrustyData est une API REST de qualité de données pour les adresses françaises, construite sur la Base Adresse Nationale et les référentiels INSEE. Plan Discovery gratuit disponible — 5 000 requêtes/mois, sans carte bancaire.