Chaque organisation qui collecte des données d'adresses depuis plusieurs années rencontre le même problème : une grande table d'adresses désordonnée avec une qualité inconnue. Des adresses saisies via des formulaires web par des utilisateurs inattentifs. Des enregistrements migrés depuis un ancien système CRM qui ne validait pas les entrées. Des importations en masse depuis une feuille Excel d'un partenaire dans un format incompatible. Des localisations professionnelles qui ont changé depuis la collecte initiale des données.
Faire de la logistique sur des données d'adresses incorrectes entraîne des livraisons échouées. Faire du marketing sur de mauvaises données gaspille le budget de courrier direct. Faire des évaluations immobilières sur de mauvaises données produit des analyses de zone de chalandise incorrectes. Avant que ces opérations ne surviennent, les adresses doivent être nettoyées et validées.
Le géocodage est le moyen le plus efficace de faire cela à grande échelle. Envoyez une chaîne d'adresse brute à l'API de géocodage, recevez en retour des composants structurés, des coordonnées géographiques et un score de confiance. Les adresses qui se géocodent correctement avec une confiance élevée sont presque certainement valides. Les adresses qui retournent une confiance faible ou aucune correspondance du tout nécessitent un examen humain.
Ce didacticiel construit un script Python complet qui lit un CSV d'adresses, géocode chacun d'eux contre l'API de géocodage MapAtlas, écrit les résultats avec des scores de confiance et signale les enregistrements à faible confiance pour un examen manuel. Il gère la limitation du débit, les erreurs réseau transitoires et les particularités du format d'adresse européen que les didacticiels de géocodage centrés sur les États-Unis oublient.
Pourquoi les données d'adresses se dégradent
La qualité des adresses se dégrade pour des raisons prévisibles :
Erreurs de saisie manuelle. Les utilisateurs de formulaires web tapent rapidement, l'autocorrection massacre les noms de rues, et la validation qui accepte n'importe quelle chaîne non vide laisse passer des données invalides. Une étude des données de caisses B2C a révélé que 7 à 12 % des adresses saisies manuellement contiennent des erreurs suffisamment importantes pour causer une défaillance de livraison.
Relocalisations professionnelles. Une base de données B2B collectée il y a deux ans aura environ 10 à 15 % des adresses qui ne correspondent plus à la localisation professionnelle actuelle en raison de changements, fusions et fermetures.
Incohérences de format. Les données collectées à partir de multiples sources utilisent des conventions différentes : noms de pays complets vs codes ISO, « St. » vs « Street », numéro de maison avant/après la rue, « flat » vs « apt » vs « wohnung ». Un géocodeur normalise tout cela en sortie structurée.
Migrations de systèmes hérités. Les champs d'adresse qui étaient répartis sur plusieurs colonnes de base de données obtiennent souvent une concaténation lors de la migration, perdant la structure. Les champs d'adresse en texte libre des anciens systèmes peuvent inclure des notes, des références ou une mise en forme qui ne font pas partie de l'adresse réelle.
L'approche du géocodage gère tous ces cas car elle s'appuie sur la correspondance géographique, pas sur la correspondance de chaînes. Une adresse mal formatée peut toujours se géocoder correctement si les données de localisation sous-jacentes correspondent.
Comprendre les scores de confiance
L'API de géocodage MapAtlas retourne une propriété confidence sur chaque entité, allant de 0,0 à 1,0. Elle représente comment le résultat retourné correspond étroitement à la requête d'entrée, en tenant compte des différences de format, des abréviations et de l'ambiguïté.
| Confiance | Interprétation | Action recommandée |
|---|---|---|
| 0,90 à 1,00 | Correspondance exacte ou quasi-exacte | Accepter automatiquement |
| 0,85 à 0,89 | Correspondance forte, différences de format mineures | Accepter avec journalisation |
| 0,60 à 0,84 | Correspondance partielle, rue trouvée mais numéro de maison incertain | Signaler pour examen manuel |
| 0,40 à 0,59 | Ambigu, localité trouvée mais pas d'adresse spécifique | Rejeter ou escalader |
| 0,00 à 0,39 | Aucune correspondance significative | Rejeter, probablement invalide |
Ces seuils sont des points de départ. Pour une opération logistique où les livraisons échouées coûtent cher, augmentez le seuil d'acceptation automatique à 0,92+. Pour une liste de diffusion marketing où le coût d'un examen manuel surpasse le coût de quelques mauvaises adresses, diminuez-le à 0,80.
L'API retourne également une propriété match_type indiquant quel niveau de la hiérarchie d'adresses a correspondu : point (bâtiment exact), interpolated (position estimée entre les numéros de maison connus), street (rue trouvée mais numéro de maison non) ou locality (seulement la ville a correspondu).
Le script de validation Python
Installez les dépendances :
pip install requests pandas tqdm
Le script lit un CSV avec une colonne address (ou des colonnes street, city, country séparées), géocode chaque ligne et écrit un nouveau CSV avec les résultats de validation ajoutés.
#!/usr/bin/env python3
"""
bulk_geocode.py, Validate a CSV of addresses using the MapAtlas Geocoding API.
Input CSV must have either:
- An 'address' column (full address string), or
- 'street', 'city', and 'country' columns (will be concatenated)
Outputs a new CSV with added columns:
geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
"""
import csv
import time
import logging
import requests
import pandas as pd
from tqdm import tqdm
from pathlib import Path
# ── Configuration ──────────────────────────────────────────────────────────────
API_KEY = 'YOUR_API_KEY'
API_BASE = 'https://api.mapatlas.eu/geocoding/v1/search'
INPUT_CSV = 'addresses.csv'
OUTPUT_CSV = 'addresses_validated.csv'
RATE_LIMIT_RPS = 5 # Requests per second (stay within your plan limits)
RETRY_ATTEMPTS = 3 # Retries on transient errors
RETRY_DELAY_S = 2.0 # Seconds between retries
# Confidence thresholds
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)
# ── Geocoding function ─────────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
"""
Geocode a single address string. Returns a dict with result fields.
Retries on network errors and 429 rate-limit responses.
"""
params = {
'text': address_str,
'key': API_KEY,
'size': 1,
}
for attempt in range(RETRY_ATTEMPTS):
try:
resp = requests.get(API_BASE, params=params, timeout=10)
if resp.status_code == 429:
# Rate limited, wait and retry
wait = float(resp.headers.get('Retry-After', RETRY_DELAY_S * (attempt + 1)))
log.warning(f'Rate limited. Waiting {wait:.1f}s before retry {attempt + 1}.')
time.sleep(wait)
continue
resp.raise_for_status()
data = resp.json()
if not data.get('features'):
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'no_match', 'status': 'reject'}
feature = data['features'][0]
props = feature['properties']
coords = feature['geometry']['coordinates'] # [lng, lat]
confidence = round(float(props.get('confidence', 0.0)), 4)
match_type = props.get('match_type', 'unknown')
if confidence >= ACCEPT_THRESHOLD:
status = 'accept'
elif confidence >= REVIEW_THRESHOLD:
status = 'review'
else:
status = 'reject'
return {
'geocoded_label': props.get('label', ''),
'geocoded_lat': round(coords[1], 6),
'geocoded_lng': round(coords[0], 6),
'confidence': confidence,
'match_type': match_type,
'status': status,
}
except requests.exceptions.RequestException as exc:
log.warning(f'Network error on attempt {attempt + 1}: {exc}')
if attempt < RETRY_ATTEMPTS - 1:
time.sleep(RETRY_DELAY_S * (attempt + 1))
# All retries exhausted
return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
'confidence': 0.0, 'match_type': 'error', 'status': 'error'}
# ── Main processing loop ───────────────────────────────────────────────────────
def main():
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# Build address string from available columns
if 'address' in df.columns:
df['_query'] = df['address']
elif all(c in df.columns for c in ['street', 'city', 'country']):
df['_query'] = df['street'] + ', ' + df['city'] + ', ' + df['country']
else:
raise ValueError("CSV must have 'address' or 'street'+'city'+'country' columns.")
results = []
sleep_interval = 1.0 / RATE_LIMIT_RPS
for query in tqdm(df['_query'], desc='Geocoding', unit='addr'):
result = geocode_address(query.strip())
results.append(result)
time.sleep(sleep_interval)
results_df = pd.DataFrame(results)
output_df = pd.concat([df.drop(columns=['_query']), results_df], axis=1)
output_df.to_csv(OUTPUT_CSV, index=False, quoting=csv.QUOTE_NONNUMERIC)
# Summary
total = len(output_df)
accept = (output_df['status'] == 'accept').sum()
review = (output_df['status'] == 'review').sum()
reject = (output_df['status'] == 'reject').sum()
errors = (output_df['status'] == 'error').sum()
log.info(f'\n── Results ────────────────────────────────')
log.info(f'Total processed : {total:,}')
log.info(f'Accept (≥{ACCEPT_THRESHOLD}) : {accept:,} ({accept/total:.1%})')
log.info(f'Review : {review:,} ({review/total:.1%})')
log.info(f'Reject : {reject:,} ({reject/total:.1%})')
log.info(f'Errors : {errors:,} ({errors/total:.1%})')
log.info(f'Output written to {OUTPUT_CSV}')
if __name__ == '__main__':
main()
Exécutez-le :
python bulk_geocode.py
Pour un CSV de 10 000 adresses à 5 requêtes/seconde, le script se termine en environ 35 minutes. La barre de progression (via tqdm) affiche le débit en temps réel et l'heure estimée d'achèvement.
Traiter la sortie
Le CSV de sortie ajoute six colonnes à vos données d'entrée :
address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status
Filtrez par statut pour générer trois fichiers de sortie :
# Après avoir exécuté main(), divisez en niveaux d'acceptation :
df = pd.read_csv('addresses_validated.csv')
df[df['status'] == 'accept'].to_csv('addresses_clean.csv', index=False)
df[df['status'] == 'review'].to_csv('addresses_review.csv', index=False)
df[df['status'] == 'reject'].to_csv('addresses_reject.csv', index=False)
print(f"Propres : {len(df[df['status']=='accept']):,} adresses prêtes à l'emploi")
print(f"Examen : {len(df[df['status']=='review']):,} adresses à vérifier manuellement")
print(f"Rejet : {len(df[df['status']=='reject']):,} adresses à rejeter ou corriger")
Le fichier addresses_review.csv est celui qui nécessite une attention humaine. Modèles d'examen typiques :
- Type de correspondance
street(confiance 0,65 à 0,80) : La rue a été trouvée mais pas le numéro de maison. Probablement un bâtiment nouveau, une adresse rurale avec une couverture clairsemée, ou une typo dans le numéro de maison. Vérifiez la source originale. - Type de correspondance
locality(confiance 0,50 à 0,65) : Seulement la ville a correspondu. Le nom de la rue est probablement mal orthographié ou n'existe pas dans les données. Recherchez l'adresse dans un répertoire postal. - Faible confiance sur des adresses clairement valides : Vérifiez une inadéquation de pays/langue. Une adresse néerlandaise interrogée sans restriction de pays peut se géocoder par rapport à une ville allemande ou belge de même nom.
Particularités du format d'adresse européen à connaître
Les guides de géocodage en masse écrits pour le marché américain omettent les différences de format qui font trébucher les ensembles de données européens. Voici les problèmes les plus courants :
Allemagne, ordre des numéros de maison. Les adresses allemandes utilisent le format {street} {number} : Berliner Straße 42. De nombreux CRM stockent les adresses au format {number} {street} car ils ont été construits selon la convention britannique/américaine. Si vos adresses allemandes se géocodent avec une faible confiance, essayez d'inverser le numéro et le nom de rue dans la chaîne de requête avant soumission.
France, arrondissements. Les adresses parisiennes incluent un arrondissement (1er à 20e) dans le code postal : 75001 à 75020. Les requêtes qui omettent l'arrondissement et utilisent juste Paris sont géocodées au centre-ville, pas au district spécifique, cela apparaît comme une correspondance locality avec faible confiance plutôt qu'une correspondance address.
Pays-Bas, format de code postal. Les codes postaux néerlandais suivent un modèle strict DDDD LL (4 chiffres, espace, 2 lettres majuscules). Les codes stockés sans espace (1012LG au lieu de 1012 LG) ou avec des lettres minuscules se géocodent correctement, mais si vous validez le format du code postal séparément, normalisez à majuscules avec un espace.
Belgique, ambiguïté linguistique. Certaines municipalités belges ont des noms français et néerlandais différents (Liège/Luik, Gent/Gand). L'API gère les deux, mais la dénomination incohérente dans votre ensemble de données (certains enregistrements utilisant le français, d'autres le néerlandais) peut produire des niveaux de confiance différents. Normalisez à une langue par région avant le géocodage.
Espagne et Italie, variations de préfixes de rue. « Calle », « Carrer », « Via », « Viale » sont tous des préfixes de type de rue valides et peuvent apparaître abrégés (« C/ ») dans les enregistrements. Le géocodeur gère les abréviations courantes mais les raccourcis peu courants des systèmes hérités peuvent nécessiter une étape de normalisation.
Augmenter le débit avec des requêtes concurrentes
Le script séquentiel est sûr et simple mais lent. Pour les ensembles de données plus grands, remplacez la boucle séquentielle par un exécuteur concurrent :
from concurrent.futures import ThreadPoolExecutor, as_completed
def main_concurrent(max_workers=10):
df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
# ... (même configuration qu'avant) ...
results = [None] * len(df)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_idx = {
executor.submit(geocode_address, row['_query'].strip()): idx
for idx, row in df.iterrows()
}
for future in tqdm(as_completed(future_to_idx), total=len(df), desc='Géocodage'):
idx = future_to_idx[future]
results[idx] = future.result()
# ... (même écriture de sortie qu'avant) ...
Avec 10 workers concurrents à 5 RPS par worker, le débit atteint environ 50 requêtes/seconde, 10 000 adresses en moins de 4 minutes. Vérifiez la limite de requêtes concurrentes de votre plan MapAtlas avant d'augmenter max_workers.
Pour les organisations exécutant une validation d'adresses en tant qu'opération récurrente, nettoyant les nouvelles importations de CRM chaque semaine ou validant les adresses de livraison chaque nuit, consultez la page des solutions logistique et livraison pour voir comment les intégrations MapAtlas s'intègrent aux workflows opérationnels.
Si vous construisez l'autocomplétion d'adresse pour éviter que les mauvaises données n'entrent dans votre système en premier lieu (capturer les erreurs à la source plutôt que de les nettoyer par lot), l'API Autocomplète d'adresse : comment un champ augmente la conversion de casse de 35 % couvre l'implémentation frontend.
Travail avec la sortie des coordonnées
La sortie validée inclut geocoded_lat et geocoded_lng pour chaque adresse acceptée. Ces coordonnées ouvrent des capacités d'analyse qui n'étaient pas possibles avec les chaînes d'adresses brutes :
- Calculs de distance. Calculez les distances en ligne droite entre un entrepôt et chaque adresse de livraison pour estimer les niveaux de coûts d'expédition.
- Analyse de zone de chalandise. Tracez les localisations client validées sur une carte pour voir où la demande se concentre géographiquement.
- Attribution de zone de livraison. Attribuez chaque adresse à une zone de livraison en testant si ses coordonnées se situent dans un polygone de zone.
- Détection de doublons. Deux enregistrements avec les mêmes coordonnées (à quelques mètres près) sont probablement des doublons, même si les chaînes d'adresses diffèrent par la mise en forme.
Pour la cohérence NAP (Nom/Adresse/Téléphone) et son impact sur la visibilité de la recherche par IA, particulièrement pertinente pour les bases de données d'adresses professionnelles locales, Cohérence NAP pour la recherche par IA : pourquoi les adresses mal assorties tuent votre visibilité ChatGPT explique pourquoi les adresses validées par géocodage sont la bonne base pour le balisage des données structurées.
Résumé
Le géocodage en masse avec l'API de géocodage MapAtlas vous donne :
- Adresses validées avec des scores de confiance pour savoir quels enregistrements approuver automatiquement et lesquels nécessitent un examen.
- Composants structurés (rue, numéro de maison, code postal, ville, pays) normalisés à partir de n'importe quel format d'entrée.
- Coordonnées géographiques pour chaque adresse acceptée, permettant l'analyse spatiale et le routage.
- Support du format d'adresse européen intégré à l'API, pas quelque chose que vous devez gérer en prétraitement.
Le script Python s'exécute à 5 adresses/seconde séquentiellement, jusqu'à 50/seconde avec des workers concurrents. Pour 10 000 adresses, budgétisez 4 à 40 minutes selon la concurrence. La sortie est un CSV propre avec trois niveaux : accepter, examiner, rejeter.
Inscrivez-vous pour une clé API MapAtlas gratuite pour commencer. L'API de géocodage supporte les requêtes en masse sur tous les plans, consultez tarification pour les tarifs par requête et les limites de niveau gratuit mensuel.

