Skip to main content
Validasi Alamat Massal dengan Geocoding API: Proses 10.000 Alamat Sekaligus
Tutorials

Validasi Alamat Massal dengan Geocoding API: Proses 10.000 Alamat Sekaligus

Validasi alamat massal di Python dengan MapAtlas Geocoding API: rate limiting, logika retry, confidence scoring, dan format alamat EU untuk 10.000+ record.

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

Setiap organisasi yang telah mengumpulkan data alamat selama lebih dari beberapa tahun memiliki masalah yang sama: tabel alamat besar yang berantakan dengan kualitas tidak diketahui. Alamat yang dimasukkan melalui formulir web oleh pengguna yang tidak perhatian. Catatan yang dimigrasikan dari CRM warisan yang tidak memvalidasi input. Impor massal dari lembar Excel mitra dalam format yang tidak kompatibel. Lokasi bisnis yang telah pindah sejak data dikumpulkan.

Menjalankan logistik pada data alamat buruk menyebabkan pengiriman gagal. Menjalankannya pada pemasaran membuang pengeluaran surat langsung. Menjalankan valuasi real estat padanya menghasilkan analisis area tangkapan yang salah. Sebelum operasi apa pun terjadi, alamat perlu dibersihkan dan divalidasi.

Geocoding adalah cara paling efektif untuk melakukan ini dalam skala besar. Kirim string alamat mentah ke API Geocoding; terima kembali komponen terstruktur, koordinat geografis, dan skor kepercayaan. Alamat yang geocode dengan bersih dengan kepercayaan tinggi hampir pasti valid. Alamat yang mengembalikan kepercayaan rendah atau tidak ada kecocokan sama sekali memerlukan tinjauan manusia.

Tutorial ini membangun skrip Python lengkap yang membaca CSV alamat, geocodes masing-masing terhadap MapAtlas Geocoding API, menulis hasil dengan skor kepercayaan, dan menandai catatan kepercayaan rendah untuk tinjauan manual. Ini menangani pembatasan laju, kesalahan jaringan sementara, dan keanehan format alamat EU yang tutorial geocoding berpusat pada AS lewatkan.

Mengapa Data Alamat Menjadi Buruk

Kualitas alamat menurun karena alasan yang dapat diprediksi:

Kesalahan entri manual. Pengguna formulir web mengetik dengan cepat, autocorrect menghancurkan nama jalan, dan validasi yang menerima string tidak kosong apa pun membiarkan sampah lewat. Studi data checkout B2C menemukan bahwa 7-12% dari alamat yang dimasukkan secara manual mengandung kesalahan cukup signifikan untuk menyebabkan kegagalan pengiriman.

Relokasi bisnis. Database B2B yang dikumpulkan dua tahun lalu akan memiliki sekitar 10-15% dari alamat yang tidak lagi cocok dengan lokasi bisnis saat ini karena perpindahan, penggabungan, dan penutupan.

Inkonsistensi format. Data yang dikumpulkan dari berbagai sumber menggunakan konvensi berbeda: nama negara lengkap vs. kode ISO, "St." vs. "Street", nomor rumah sebelum jalan vs. sesudah, "flat" vs. "apt" vs. "wohnung". Geocoder menormalkan semua ini menjadi output terstruktur.

Migrasi sistem warisan. Bidang alamat yang dibagi di beberapa kolom database sering dikoncatenasi selama migrasi, kehilangan struktur. Bidang alamat teks bebas dari sistem yang lebih tua dapat mencakup catatan, referensi, atau pemformatan yang bukan bagian dari alamat aktual.

Pendekatan geocoding menangani semua ini karena mengandalkan pencocokan geografis, bukan pencocokan string. Alamat yang diformat secara salah masih dapat geocode dengan benar jika data lokasi yang mendasari cocok.

Memahami Skor Kepercayaan

API Geocoding MapAtlas mengembalikan properti confidence pada setiap fitur, mulai dari 0,0 hingga 1,0. Ini mewakili seberapa dekat hasil yang dikembalikan cocok dengan kueri input, memperhitungkan perbedaan format, singkatan, dan ambiguitas.

KepercayaanInterpretasiTindakan yang direkomendasikan
0,90 – 1,00Kecocokan yang tepat atau hampir tepatTerima secara otomatis
0,85 – 0,89Kecocokan kuat, perbedaan format kecilTerima dengan pencatatan
0,60 – 0,84Kecocokan sebagian, jalan ditemukan tetapi nomor rumah tidak pastiBendera untuk tinjauan manual
0,40 – 0,59Ambigu, kecocokan lokal tetapi bukan alamat spesifikTolak atau eskalasi
0,00 – 0,39Tidak ada kecocokan yang berartiTolak, kemungkinan tidak valid

Ambang batas ini adalah titik awal. Untuk operasi logistik di mana pengiriman gagal mahal, ketatkan ambang penerimaan otomatis menjadi 0,92+. Untuk daftar surat pemasaran di mana biaya tinjauan manual melebihi biaya beberapa alamat buruk, longgarkan menjadi 0,80.

API juga mengembalikan properti match_type yang menunjukkan tingkat apa dari hirarki alamat yang cocok: point (bangunan yang tepat), interpolated (posisi diperkirakan antara nomor rumah yang diketahui), street (jalan ditemukan tetapi nomor rumah tidak), atau locality (hanya kota yang cocok).

Skrip Validasi Python

Pasang dependensi:

pip install requests pandas tqdm

Skrip membaca CSV dengan kolom address (atau kolom street, city, country terpisah), geocodes setiap baris, dan menulis CSV baru dengan hasil validasi ditambahkan.

#!/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()

Jalankan:

python bulk_geocode.py

Untuk CSV 10.000 alamat dengan 5 permintaan/detik, skrip selesai dalam sekitar 35 menit. Bilah kemajuan (melalui tqdm) menunjukkan throughput real-time dan perkiraan waktu penyelesaian.

Menangani Output

Output CSV menambahkan enam kolom ke data input Anda:

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

Filter berdasarkan status untuk menghasilkan tiga file output:

# Setelah menjalankan main(), bagi menjadi tingkatan penerimaan:
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"Clean:  {len(df[df['status']=='accept']):,} addresses ready for use")
print(f"Review: {len(df[df['status']=='review']):,} addresses for manual check")
print(f"Reject: {len(df[df['status']=='reject']):,} addresses to discard or fix")

File addresses_review.csv adalah file yang membutuhkan perhatian manusia. Pola tinjauan umum:

  • Tipe kecocokan street (kepercayaan 0,65-0,80): Jalan ditemukan tetapi bukan nomor rumah. Kemungkinan bangunan baru, alamat pedesaan dengan cakupan jarang, atau typo dalam nomor rumah. Periksa sumber asli.
  • Tipe kecocokan locality (kepercayaan 0,50-0,65): Hanya kota yang cocok. Nama jalan kemungkinan salah eja atau tidak ada dalam data. Cari alamat di direktori pos.
  • Kepercayaan rendah pada alamat yang terlihat jelas valid: Periksa ketidakcocokan negara/bahasa. Alamat Belanda yang dikueri tanpa pembatasan negara dapat geocode terhadap kota Jerman atau Belgia dengan nama serupa.

Keanehan Alamat EU yang Perlu Diketahui

Panduan geocoding massal yang ditulis untuk pasar AS melewatkan perbedaan format yang mengganggu dataset EU. Berikut adalah masalah paling umum:

Jerman, pengurutan nomor rumah. Alamat Jerman menggunakan format {street} {number}: Berliner Straße 42. Banyak CRM menyimpan alamat dalam format {number} {street} karena dibangun untuk konvensi UK/AS. Jika alamat Jerman Anda geocoding dengan kepercayaan rendah, coba balikkan nomor dan nama jalan dalam string kueri sebelum pengiriman.

Prancis, arrondissements. Alamat Paris mencakup arrondissement (1-20) sebagai bagian dari kode pos: 75001 hingga 75020. Kueri yang menghilangkan arrondissement dan hanya menggunakan Paris geocode ke centroid kota, bukan distrik tertentu, ini muncul sebagai kecocokan locality dengan kepercayaan rendah bukan kecocokan address.

Belanda, format kode pos. Kode pos Belanda mengikuti pola ketat DDDD LL (4 digit, spasi, 2 huruf besar). Kode yang disimpan tanpa spasi (1012LG bukan 1012 LG) atau dengan huruf kecil akan geocode dengan benar, tetapi jika Anda memvalidasi format kode pos secara terpisah, normalisasi menjadi huruf besar dengan spasi.

Belgia, ambiguitas bahasa. Beberapa kota Belgia memiliki nama Prancis dan Belanda berbeda (Liège/Luik, Gent/Gand). API menangani keduanya, tetapi penamaan tidak konsisten dalam dataset Anda (beberapa catatan menggunakan Prancis, beberapa Belanda) dapat menghasilkan tingkat kepercayaan berbeda. Normalisasi ke satu bahasa per region sebelum geocoding.

Spanyol dan Italia, variasi awalan jalan. "Calle", "Carrer", "Via", "Viale" adalah semua awalan tipe jalan yang valid dan mungkin muncul disingkat ("C/") dalam catatan. Geocoder menangani singkatan umum tetapi pemendekan tidak biasa dari sistem warisan mungkin memerlukan langkah normalisasi.

Meningkatkan Throughput Dengan Permintaan Bersamaan

Skrip sekuensial aman dan sederhana tetapi lambat. Untuk dataset yang lebih besar, ganti loop sekuensial dengan executor bersamaan:

from concurrent.futures import ThreadPoolExecutor, as_completed

def main_concurrent(max_workers=10):
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
    # ... (setup yang sama seperti sebelumnya) ...

    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()

    # ... (penulisan output yang sama seperti sebelumnya) ...

Dengan 10 worker bersamaan pada 5 RPS per worker, throughput mencapai sekitar 50 permintaan/detik, 10.000 alamat dalam waktu kurang dari 4 menit. Periksa batas permintaan bersamaan paket MapAtlas Anda sebelum meningkatkan max_workers.

Untuk organisasi yang menjalankan validasi alamat sebagai operasi berulang, membersihkan impor CRM baru mingguan, atau memvalidasi alamat pengiriman setiap malam, lihat halaman solusi Logistik & Pengiriman untuk cara integrasi MapAtlas cocok dengan alur kerja operasional.

Jika Anda membangun pelengkapan otomatis alamat untuk mencegah data buruk memasuki sistem Anda sejak awal (menangkap kesalahan pada sumbernya daripada membersihkannya dalam batch), Address Autocomplete API: Bagaimana Satu Bidang Meningkatkan Konversi Checkout sebesar 35% mencakup implementasi frontend.

Bekerja Dengan Output Koordinat

Output tervalidasi mencakup geocoded_lat dan geocoded_lng untuk setiap alamat yang diterima. Koordinat ini membuka kemampuan analisis yang sebelumnya tidak mungkin dengan string alamat mentah:

  • Perhitungan jarak. Hitung jarak garis lurus antara gudang dan setiap alamat pengiriman untuk memperkirakan tingkat biaya pengiriman.
  • Analisis area tangkapan. Plot lokasi pelanggan tervalidasi di peta untuk melihat di mana permintaan terkonsentrasi secara geografis.
  • Penugasan zona pengiriman. Tetapkan setiap alamat ke zona pengiriman dengan menguji apakah koordinatnya berada di dalam poligon zona.
  • Deteksi duplikat. Dua catatan dengan koordinat yang sama (dalam beberapa meter) kemungkinan besar duplikat, bahkan jika string alamat berbeda dalam pemformatan.

Untuk konsistensi NAP (Nama/Alamat/Telepon) dan dampaknya pada visibilitas pencarian AI, terutama relevan untuk database alamat bisnis lokal, NAP Consistency for AI Search: Why Mismatched Addresses Kill Your ChatGPT Visibility menjelaskan mengapa alamat tervalidasi geocoding adalah fondasi yang tepat untuk markup data terstruktur.

Ringkasan

Geocoding massal dengan MapAtlas Geocoding API memberi Anda:

  • Alamat tervalidasi dengan skor kepercayaan sehingga Anda tahu catatan mana yang dapat dipercaya secara otomatis dan mana yang memerlukan tinjauan.
  • Komponen terstruktur (jalan, nomor rumah, kode pos, kota, negara) dinormalkan dari format input apa pun.
  • Koordinat geografis untuk setiap alamat yang diterima, memungkinkan analisis dan routing spasial.
  • Dukungan format alamat EU tertanam dalam API, bukan sesuatu yang perlu Anda tangani dalam pra-pemrosesan.

Skrip Python berjalan pada 5 alamat/detik secara sekuensial, hingga 50/detik dengan worker bersamaan. Untuk 10.000 alamat, anggaran 4-40 menit tergantung pada keseriusan. Output adalah CSV yang bersih dengan tiga tingkatan: terima, tinjau, tolak.


Daftar kunci API MapAtlas gratis untuk memulai. API Geocoding mendukung kueri massal di semua paket, lihat harga untuk tarif per-permintaan dan batas tingkat gratis bulanan.

Merasa ini berguna? Bagikan.

Tentang penulis

Brent van der Heiden

Ditulis oleh

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.

Lihat semua artikel
Kembali ke blog