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.
| Konfidenz | Interpretation | Empfohlene Aktion |
|---|---|---|
| 0,90 bis 1,00 | Exakte oder nahezu exakte Übereinstimmung | Automatisch akzeptieren |
| 0,85 bis 0,89 | Starke Übereinstimmung, kleine Formatunterschiede | Mit Protokollierung akzeptieren |
| 0,60 bis 0,84 | Teilweise Übereinstimmung, Straße gefunden aber Hausnummer unsicher | Zur manuellen Überprüfung kennzeichnen |
| 0,40 bis 0,59 | Mehrdeutig, Ort gefunden aber nicht spezifische Adresse | Ablehnen oder eskalieren |
| 0,00 bis 0,39 | Keine sinnvolle Übereinstimmung | Ablehnen, 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.

