Skip to main content
Validation d'adresses en masse avec la Geocoding API : 10 000 adresses en une seule passe
Tutorials

Validation d'adresses en masse avec la Geocoding API : 10 000 adresses en une seule passe

Validation d'adresses en masse en Python avec la Geocoding API MapAtlas : rate limiting, retries, scoring de confiance et formats d'adresses UE pour plus de 10 000 enregistrements.

Brent van der Heiden13 min read
#bulk geocoding#geocoding api#address validation#python geocoding#batch geocoding#address cleaning

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. Lors des tests à grande échelle du Baymard Institute sur les tunnels de paiement, 9 % des adresses de livraison saisies manuellement contenaient des fautes de frappe ou d'orthographe.

Relocalisations professionnelles. Les entreprises déménagent, fusionnent et ferment en permanence. Eurostat situe le taux de disparition des entreprises dans l'UE à 8,5 % en 2023, sans compter les déménagements : une base de données B2B collectée il y a deux ans contiendra donc une part non négligeable d'adresses qui ne correspondent plus à une entreprise en activité.

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é.

ConfianceInterprétationAction recommandée
0,90 à 1,00Correspondance exacte ou quasi-exacteAccepter automatiquement
0,85 à 0,89Correspondance forte, différences de format mineuresAccepter avec journalisation
0,60 à 0,84Correspondance partielle, rue trouvée mais numéro de maison incertainSignaler pour examen manuel
0,40 à 0,59Ambigu, localité trouvée mais pas d'adresse spécifiqueRejeter ou escalader
0,00 à 0,39Aucune correspondance significativeRejeter, 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 : exact (l'adresse complète a été trouvée), interpolated (position estimée entre les numéros de maison connus) ou fallback (l'adresse complète n'a pas été trouvée et un niveau plus large a correspondu à la place). La propriété layer indique de quel niveau il s'agit, par exemple address, street ou locality, et accuracy précise si le point est un point exact ou un centroid.

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://gateway.mapmetrics-atlas.net/forward-geocode/'
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,
        'token': 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 a besoin d'au moins 33 minutes de pause imposée par la limite de débit, plus la latence de chaque requête, soit généralement 35 à 50 minutes au total. 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 :

  • Repli sur la couche 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.
  • Repli sur la couche 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, le débit dépend de la latence de l'API et de la limite de débit de votre plan plutôt que d'une pause : à 50 requêtes par seconde, 10 000 adresses sont traitées en moins de 4 minutes. La version concurrente n'intègre aucune régulation du débit, ajoutez-en une ou réduisez max_workers si votre plan plafonne les requêtes par seconde. 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 d'autocomplétion d'adresse : comment un seul champ améliore la conversion au paiement 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, prévoyez 4 à 50 minutes selon la concurrence et la latence. 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.

Questions fréquemment posées

Combien de temps faut-il pour géocoder 10 000 adresses avec l'API MapAtlas ?

À 5 requêtes par seconde, 10 000 adresses demandent au moins 33 minutes de pause imposée par la limite de débit, plus la latence réseau de chaque requête : prévoyez donc 35 à 50 minutes. Avec 10 workers concurrents à environ 50 requêtes par seconde, on descend sous les 5 minutes, à condition que la limite de débit de votre plan le permette. Le script Python de ce tutoriel propose les deux options, séquentielle et concurrente.

Quel seuil de score de confiance utiliser pour valider des adresses ?

Un système à trois niveaux fonctionne bien : acceptez les adresses dont la confiance dépasse 0,85, signalez-les pour une revue manuelle entre 0,60 et 0,85 et rejetez-les en dessous de 0,60. Les seuils exacts dépendent de l'importance de la précision des adresses pour votre cas d'usage : la logistique doit appliquer des seuils plus stricts que les campagnes marketing.

La Geocoding API MapAtlas gère-t-elle les formats d'adresse européens ?

Oui. L'API interprète correctement les conventions allemandes (numéro après la rue), françaises (numéro avant la rue), néerlandaises (codes postaux 4+2) et celles des autres pays de l'UE. Les requêtes dans la langue locale donnent de meilleurs résultats que les requêtes translittérées ou au format anglais.

Vous avez trouvé cela utile ? Partagez-le.

À propos de l'auteur

Brent van der Heiden

Rédigé par

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

Voir tous les articles →
Retour au blog