Skip to main content
Massen-Adressvalidierung mit der Geocoding API: 10.000 Adressen auf einen Schlag prüfen
Tutorials

Massen-Adressvalidierung mit der Geocoding API: 10.000 Adressen auf einen Schlag prüfen

Massen-Adressvalidierung in Python mit der MapAtlas Geocoding API: Rate Limiting, Retry-Logik, Confidence-Scoring und EU-Adressformate für 10.000+ Datensätze.

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

Jede Organisation, die seit mehr als ein paar Jahren Adressdaten erfasst, hat dasselbe Problem: eine große, unordentliche Adresstabelle mit unbekannter Qualität. Adressen, die über Webformulare von unaufmerksamen Nutzern eingegeben wurden. Datensätze, die aus einem alten CRM migriert wurden, das keine Eingaben validierte. Massenimporte aus der Excel-Tabelle eines Partners in einem inkompatiblen Format. Unternehmensstandorte, die sich seit der Datenerfassung verändert haben.

Logistik mit fehlerhaften Adressdaten zu betreiben führt zu fehlgeschlagenen Lieferungen. Marketing damit zu betreiben verschwendet Direktmailing-Budget. Immobilienbewertungen damit durchzuführen liefert falsche Einzugsgebietsanalysen. Bevor diese Vorgänge stattfinden, müssen die Adressen bereinigt und validiert werden.

Geocoding ist der effektivste Weg, das in großem Maßstab zu tun. Senden Sie einen rohen Adressstring an die Geocoding API und erhalten Sie strukturierte Komponenten, geografische Koordinaten und einen Konfidenz-Score zurück. Adressen, die mit hoher Konfidenz sauber geocodiert werden, sind fast sicher gültig. Adressen, die eine niedrige Konfidenz oder gar keine Übereinstimmung zurückgeben, müssen manuell überprüft werden.

Dieses Tutorial erstellt ein vollständiges Python-Skript, das eine CSV-Datei mit Adressen liest, jede Adresse gegen die MapAtlas Geocoding API geocodiert, die Ergebnisse mit Konfidenz-Scores schreibt und Datensätze mit niedriger Konfidenz zur manuellen Überprüfung kennzeichnet. Es verarbeitet Ratenbegrenzungen, vorübergehende Netzwerkfehler und EU-Adressformat-Besonderheiten, die US-zentrierte Geocoding-Tutorials auslassen.

Warum Adressdaten schlechter werden

Adressqualität verschlechtert sich aus vorhersehbaren Gründen:

Manuelle Eingabefehler. Webformular-Nutzer tippen schnell, Autocorrect vermurkst Straßennamen, und eine Validierung, die jeden nicht-leeren String akzeptiert, lässt Fehler durch. Eine Studie von B2C-Checkout-Daten ergab, dass 7 bis 12 % der manuell eingegebenen Adressen Fehler enthalten, die groß genug sind, um zu Lieferausfällen zu führen.

Unternehmensumzüge. Eine vor zwei Jahren erfasste B2B-Datenbank hat etwa 10 bis 15 % der Adressen, die aufgrund von Umzügen, Fusionen und Schließungen nicht mehr mit dem aktuellen Unternehmensstandort übereinstimmen.

Format-Inkonsistenzen. Daten aus mehreren Quellen verwenden unterschiedliche Konventionen: vollständige Ländernamen vs. ISO-Codes, "Str." vs. "Straße", Hausnummer vor oder nach dem Straßennamen, "Wohnung" vs. "App" vs. "Apt.". Ein Geocoder normalisiert all das in eine strukturierte Ausgabe.

Legacy-System-Migrationen. Adressfelder, die auf mehrere Datenbankspalten aufgeteilt waren, werden bei der Migration häufig verkettet und verlieren dabei die Struktur. Freitextadressfelder aus älteren Systemen können Notizen, Referenzen oder Formatierungen enthalten, die nicht Teil der eigentlichen Adresse sind.

Der Geocoding-Ansatz bewältigt all das, weil er auf geografischem Matching basiert, nicht auf String-Matching. Eine falsch formatierte Adresse kann trotzdem korrekt geocodiert werden, wenn die zugrunde liegenden Ortsdaten übereinstimmen.

Konfidenz-Scores verstehen

Die MapAtlas Geocoding API gibt eine confidence-Eigenschaft für jedes Feature zurück, die von 0,0 bis 1,0 reicht. Sie gibt an, wie gut das zurückgegebene Ergebnis mit der Eingabeabfrage übereinstimmt, unter Berücksichtigung von Formatunterschieden, Abkürzungen und Mehrdeutigkeiten.

KonfidenzInterpretationEmpfohlene Aktion
0,90 bis 1,00Exakte oder nahezu exakte ÜbereinstimmungAutomatisch akzeptieren
0,85 bis 0,89Starke Übereinstimmung, kleine FormatunterschiedeMit Protokollierung akzeptieren
0,60 bis 0,84Teilweise Übereinstimmung, Straße gefunden aber Hausnummer unsicherZur manuellen Überprüfung kennzeichnen
0,40 bis 0,59Mehrdeutig, Ort gefunden aber nicht spezifische AdresseAblehnen oder eskalieren
0,00 bis 0,39Keine sinnvolle ÜbereinstimmungAblehnen, wahrscheinlich ungültig

Diese Schwellenwerte sind Ausgangspunkte. Für einen Logistikbetrieb, bei dem fehlgeschlagene Lieferungen teuer sind, sollte der Schwellenwert für automatische Akzeptanz auf 0,92+ erhöht werden. Für eine Marketing-Mailingliste, bei der die Kosten einer manuellen Überprüfung höher sind als die Kosten einiger schlechter Adressen, kann er auf 0,80 gesenkt werden.

Die API gibt auch eine match_type-Eigenschaft zurück, die angibt, auf welcher Ebene der Adresshierarchie die Übereinstimmung gefunden wurde: point (exaktes Gebäude), interpolated (Position zwischen bekannten Hausnummern geschätzt), street (Straße gefunden, aber keine Hausnummer) oder locality (nur die Stadt stimmte überein).

Das Python-Validierungsskript

Abhängigkeiten installieren:

pip install requests pandas tqdm

Das Skript liest eine CSV-Datei mit einer address-Spalte (oder separaten street-, city-, country-Spalten), geocodiert jede Zeile und schreibt eine neue CSV-Datei mit angehängten Validierungsergebnissen.

#!/usr/bin/env python3
"""
bulk_geocode.py, Validiert eine CSV von Adressen über die MapAtlas Geocoding API.

Eingabe-CSV muss entweder haben:
  - Eine 'address'-Spalte (vollständiger Adressstring), oder
  - 'street', 'city' und 'country'-Spalten (werden verkettet)

Gibt eine neue CSV mit zusätzlichen Spalten aus:
  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

# ── Konfiguration ──────────────────────────────────────────────────────────────
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          # Anfragen pro Sekunde
RETRY_ATTEMPTS  = 3          # Wiederholungen bei vorübergehenden Fehlern
RETRY_DELAY_S   = 2.0        # Sekunden zwischen Wiederholungen

# Konfidenz-Schwellenwerte
ACCEPT_THRESHOLD = 0.85
REVIEW_THRESHOLD = 0.60

logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
log = logging.getLogger(__name__)

# ── Geocoding-Funktion ─────────────────────────────────────────────────────────
def geocode_address(address_str: str) -> dict:
    """
    Geocodiert einen einzelnen Adressstring. Gibt ein Dict mit Ergebnisfeldern zurück.
    Wiederholt bei Netzwerkfehlern und 429-Ratenbegrenzungsantworten.
    """
    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:
                wait = float(resp.headers.get('Retry-After', RETRY_DELAY_S * (attempt + 1)))
                log.warning(f'Ratenbegrenzt. Warte {wait:.1f}s vor Wiederholung {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'Netzwerkfehler bei Versuch {attempt + 1}: {exc}')
            if attempt < RETRY_ATTEMPTS - 1:
                time.sleep(RETRY_DELAY_S * (attempt + 1))

    return {'geocoded_label': '', 'geocoded_lat': None, 'geocoded_lng': None,
            'confidence': 0.0, 'match_type': 'error', 'status': 'error'}


# ── Hauptverarbeitungsschleife ───────────────────────────────────────────────
def main():
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')

    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 muss 'address' oder 'street'+'city'+'country'-Spalten haben.")

    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)

    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── Ergebnisse ────────────────────────────────')
    log.info(f'Gesamt verarbeitet : {total:,}')
    log.info(f'Akzeptiert (>={ACCEPT_THRESHOLD}) : {accept:,}  ({accept/total:.1%})')
    log.info(f'Überprüfen          : {review:,}  ({review/total:.1%})')
    log.info(f'Abgelehnt           : {reject:,}  ({reject/total:.1%})')
    log.info(f'Fehler              : {errors:,}  ({errors/total:.1%})')
    log.info(f'Ausgabe geschrieben nach {OUTPUT_CSV}')


if __name__ == '__main__':
    main()

Ausführen:

python bulk_geocode.py

Für eine CSV mit 10.000 Adressen bei 5 Anfragen/Sekunde benötigt das Skript etwa 35 Minuten. Der Fortschrittsbalken (via tqdm) zeigt den Echtzeit-Durchsatz und die geschätzte Fertigstellungszeit.

Die Ausgabe verarbeiten

Die Ausgabe-CSV fügt Ihren Eingabedaten sechs Spalten hinzu:

address, ..., geocoded_label, geocoded_lat, geocoded_lng, confidence, match_type, status

Filtern Sie nach Status, um drei Ausgabedateien zu erstellen:

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"Sauber:     {len(df[df['status']=='accept']):,} Adressen zur Verwendung bereit")
print(f"Überprüfen: {len(df[df['status']=='review']):,} Adressen für manuelle Prüfung")
print(f"Abgelehnt:  {len(df[df['status']=='reject']):,} Adressen verwerfen oder korrigieren")

Die Datei addresses_review.csv ist diejenige, die menschliche Aufmerksamkeit erfordert. Typische Überprüfungsmuster:

  • Match-Typ street (Konfidenz 0,65 bis 0,80): Die Straße wurde gefunden, aber nicht die Hausnummer. Wahrscheinlich ein neues Gebäude, eine ländliche Adresse mit geringer Abdeckung oder ein Tippfehler in der Hausnummer.
  • Match-Typ locality (Konfidenz 0,50 bis 0,65): Nur die Stadt stimmte überein. Der Straßenname ist wahrscheinlich falsch geschrieben oder existiert in den Daten nicht.
  • Niedrige Konfidenz bei eindeutig gültig aussehenden Adressen: Überprüfen Sie auf Länder-/Sprach-Mismatch. Eine niederländische Adresse ohne Länderbeschränkung kann gegen eine ähnlich benannte deutsche oder belgische Stadt geocodiert werden.

EU-Adressbesonderheiten

Deutschland, Hausnummernreihenfolge. Deutsche Adressen verwenden das Format {Straße} {Nummer}: Berliner Straße 42. Viele CRMs speichern Adressen im Format {Nummer} {Straße}. Wenn Ihre deutschen Adressen mit niedriger Konfidenz geocodiert werden, versuchen Sie, Nummer und Straßenname in der Abfragezeichenfolge umzukehren.

Frankreich, Arrondissements. Pariser Adressen enthalten ein Arrondissement als Teil der Postleitzahl: 75001 bis 75020. Abfragen ohne Arrondissement werden zum Stadtzentrum geocodiert, nicht zum spezifischen Bezirk.

Niederlande, Postleitzahlformat. Niederländische Postleitzahlen folgen dem strikten Muster DDDD LL (4 Ziffern, Leerzeichen, 2 Großbuchstaben). Codes ohne Leerzeichen werden korrekt geocodiert, aber normalisieren Sie das Format für separate Postleitzahl-Validierungen.

Belgien, Sprachambiguität. Einige belgische Gemeinden haben unterschiedliche französische und niederländische Namen (Liège/Luik, Gent/Gand). Normalisieren Sie vor dem Geocoding auf eine Sprache pro Region.

Spanien und Italien, Straßenpräfix-Variationen. "Calle", "Carrer", "Via", "Viale" können abgekürzt erscheinen. Der Geocoder verarbeitet gängige Abkürzungen, aber ungewöhnliche Kurzformen aus Legacy-Systemen können einen Normalisierungsschritt erfordern.

Durchsatz mit gleichzeitigen Anfragen erhöhen

Das sequentielle Skript ist sicher und einfach, aber langsam. Für größere Datensätze ersetzen Sie die sequentielle Schleife durch einen gleichzeitigen Executor:

from concurrent.futures import ThreadPoolExecutor, as_completed

def main_concurrent(max_workers=10):
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
    # ... (dieselbe Einrichtung wie zuvor) ...

    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='Geocoding'):
            idx = future_to_idx[future]
            results[idx] = future.result()

    # ... (dieselbe Ausgabeschreibung wie zuvor) ...

Mit 10 gleichzeitigen Workern bei 5 RPS pro Worker erreicht der Durchsatz etwa 50 Anfragen/Sekunde. Das sind 10.000 Adressen in unter 4 Minuten. Überprüfen Sie das Limit für gleichzeitige Anfragen Ihres MapAtlas-Plans, bevor Sie max_workers erhöhen.

Für Organisationen, die Adressvalidierung als wiederkehrenden Vorgang durchführen, beschreibt die Lösungsseite Logistik und Lieferung, wie MapAtlas-Integrationen in betriebliche Abläufe passen.

Wenn Sie Adress-Autocomplete einbauen möchten, um fehlerhafte Daten von vornherein zu verhindern, behandelt Address Autocomplete API: How One Field Lifts Checkout Conversion by 35% die Frontend-Implementierung.

Mit der Koordinaten-Ausgabe arbeiten

Die validierte Ausgabe enthält geocoded_lat und geocoded_lng für jede akzeptierte Adresse. Diese Koordinaten ermöglichen Analysen, die mit rohen Adressstrings nicht möglich waren:

  • Entfernungsberechnungen. Berechnen Sie Luftlinienentfernungen zwischen einem Lager und jeder Lieferadresse.
  • Einzugsgebietsanalyse. Tragen Sie validierte Kundenstandorte auf einer Karte ein, um geografische Nachfragekonzentration zu erkennen.
  • Lieferzonen-Zuweisung. Weisen Sie jeder Adresse eine Lieferzone zu, indem Sie testen, ob ihre Koordinaten innerhalb eines Zonenpolygons liegen.
  • Duplikaterkennung. Zwei Einträge mit denselben Koordinaten (innerhalb weniger Meter) sind wahrscheinlich Duplikate, auch wenn die Adressstrings unterschiedlich formatiert sind.

Für NAP-Konsistenz und deren Auswirkung auf die KI-Suchsichtbarkeit erklärt NAP Consistency for AI Search: Why Mismatched Addresses Kill Your ChatGPT Visibility, warum geocoding-validierte Adressen die richtige Grundlage für strukturierte Datenmarkierung sind.

Zusammenfassung

Massen-Geocoding mit der MapAtlas Geocoding API liefert Ihnen:

  • Validierte Adressen mit Konfidenz-Scores, sodass Sie wissen, welchen Einträgen Sie automatisch vertrauen können.
  • Strukturierte Komponenten aus jedem Eingabeformat normalisiert.
  • Geografische Koordinaten für jede akzeptierte Adresse, die Raumanalyse und Routing ermöglichen.
  • EU-Adressformat-Unterstützung in der API integriert.

Das Python-Skript läuft sequentiell bei 5 Adressen/Sekunde, bis zu 50/Sekunde mit gleichzeitigen Workern. Für 10.000 Adressen kalkulieren Sie 4 bis 40 Minuten ein. Die Ausgabe ist eine saubere CSV mit drei Ebenen: akzeptieren, überprüfen, ablehnen.


Melden Sie sich für einen kostenlosen MapAtlas API-Key an um zu starten. Die Geocoding API unterstützt Massenabfragen in allen Plänen, Preise und monatliche Gratisstufenlimits finden Sie auf der Preisseite.

Hat Ihnen das geholfen? Teilen Sie es.

Über den Autor

Brent van der Heiden

Verfasst von

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. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

Alle Artikel ansehen
Zurück zum Blog