Skip to main content
Geocoding API による住所一括検証:10,000件のアドレスを一度に検証する
Tutorials

Geocoding API による住所一括検証:10,000件のアドレスを一度に検証する

Python と MapAtlas Geocoding API による住所一括検証チュートリアル。レート制限、リトライ処理、信頼度スコアリング、EU 特有の住所フォーマットまで含め10,000件以上を処理する方法を解説します。

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

数年以上の間、アドレスデータを収集している組織はすべて、同じ問題を抱えています。品質が不明で大規模でぐちゃぐちゃなアドレステーブルです。注意を払わないユーザーによってWebフォームを通じて入力されたアドレス。入力を検証しなかったレガシーCRMから移行されたレコード。パートナーの互換性のないExcelシートからの一括インポート。データが収集されて以来、移動されたビジネスの位置。

不正なアドレスデータで物流を実行すると、配達に失敗します。それでマーケティングを実行すると、ダイレクトメール支出が無駄になります。不動産評価を実行すると、不正な商圏分析が行われます。これらの操作が行われる前に、アドレスをクリーニングして検証する必要があります。

ジオコーディングはこれを規模で行う最も効果的な方法です。生のアドレス文字列をジオコーディングAPIに送信します。構造化されたコンポーネント、地理的座標、信頼度スコアを返します。高い信頼度でクリーンにジオコードされたアドレスはほぼ確実に有効です。信頼度が低いまたはまったく一致しないアドレスは、人間のレビューが必要です。

このチュートリアルは、アドレスのCSVを読み取り、MapAtlasジオコーディングAPIに対して各アドレスをジオコードし、信頼度スコアとともに結果を書き込み、低信頼度レコードをマニュアルレビューのためにフラグする完全なPythonスクリプトを構築します。レート制限、一時的なネットワークエラー、およびUS中心のジオコーディングチュートリアルが逃すEUアドレスフォーマットの癖を処理します。

アドレスデータが悪化する理由

アドレスの品質は予測可能な理由で低下します:

手動入力エラー。 Webフォームユーザーは素早くタイプし、オートコレクトは通りの名前をすり抜け、空でない文字列を受け入れる検証はゴミを通します。B2Cチェックアウトデータの研究では、手動で入力されたアドレスの7~12%に配達の失敗を引き起こすのに十分な大きさのエラーが含まれていることがわかりました。

ビジネスの再配置。 2年前に収集されたB2Bデータベースは、移動、合併、閉鎖により、現在のビジネス位置に一致しなくなったアドレスの約10~15%を持つでしょう。

形式の矛盾。 複数のソースから収集されたデータは、異なる規則を使用します。完全な国名とISOコード、「St.」対「Street」、ハウス番号前通りと後、「flat」対「apt」対「wohnung」。ジオコーダーはこれらすべてを構造化された出力に正規化します。

レガシーシステムの移行。 複数のデータベース列に分割されたアドレスフィールドは、多くの場合、移行中に連結され、構造が失われます。古いシステムのフリーテキストアドレスフィールドには、実際のアドレスの一部ではない注記、参照、または形式が含まれる場合があります。

ジオコーディングアプローチは、文字列マッチングではなく地理的マッチングに依存しているため、これらすべてを処理します。フォーマットが正しくないアドレスでも、基盤となる位置データが一致する場合でもジオコードを正しく行うことができます。

信頼度スコアを理解する

MapAtlas ジオコーディングAPIは、各機能にconfidenceプロパティを返し、0.0から1.0の範囲で返されます。形式の違い、省略形、およびあいまいさを考慮して、返された結果が入力クエリにどの程度一致するかを表します。

信頼度解釈推奨アクション
0.90 - 1.00正確または近似の一致自動的に受け入れます
0.85 - 0.89強い一致、軽微なフォーマットの違いロギング付きで受け入れます
0.60 - 0.84部分一致、通りが見つかったがハウス番号は不確実手動レビューのためにフラグを立てます
0.40 - 0.59あいまい、地域がマッチしたが具体的なアドレスではない拒否または段階的に
0.00 - 0.39意味のある一致なし拒否、おそらく無効

これらの閾値は出発点です。失敗した配達が高い物流操作では、自動受け入れの閾値を0.92以上に厳しくしてください。マニュアルレビューのコストが悪いアドレスのいくつかのコストを上回るマーケティングメーリングリストの場合は、0.80に緩くしてください。

APIはまた、match_typeプロパティを返し、アドレス階層のどのレベルがマッチしたかを示します。point(正確な建物)、interpolated(既知のハウス番号間の推定位置)、street(通りが見つかったがハウス番号ではない)、またはlocality(都市だけがマッチした)。

Pythonの検証スクリプト

依存関係をインストールしてください:

pip install requests pandas tqdm

スクリプトはaddress列(または別のstreetcitycountry列)を持つCSVを読み取り、各行をジオコードし、検証結果を追加した新しいCSVを書き込みます。

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

実行してください:

python bulk_geocode.py

5リクエスト/秒で10,000アドレスのCSVの場合、スクリプトは約35分で完了します。進捗バー(tqdm経由)は、リアルタイムのスループットと推定完了時間を表示します。

出力の処理

出力CSVは入力データに6つの列を追加します:

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

ステータスでフィルタリングして3つの出力ファイルを生成します:

# After running main(), split into acceptance tiers:
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")

addresses_review.csvファイルは、人間の注意が必要なものです。典型的なレビューパターン:

  • マッチタイプstreet(信頼度0.65~0.80): 通りは見つかったがハウス番号は見つかりませんでした。おそらく新しい建物、スパースカバレッジを備えた農村アドレス、またはハウス番号のタイプミス。オリジナルソースを確認してください。
  • マッチタイプlocality(信頼度0.50~0.65): 都市だけがマッチしました。通り名の綴りが間違っているか、データに存在しない可能性があります。郵便ディレクトリでアドレスを検索してください。
  • 明らかに有効に見えるアドレスの信頼度が低い: 国/言語の不一致をチェックしてください。国の制限なしでクエリされたオランダのアドレスは、同様の名前のドイツまたはベルギーの町に対してジオコードできます。

知っておくべきEUアドレスの癖

米国市場向けに書かれたバルクジオコーディングガイドは、EUデータセットを引っかかる形式の違いをスキップします。最も一般的な問題は次のとおりです:

ドイツ、ハウス番号の順序。 ドイツのアドレスは{street} {number}フォーマットを使用します:Berliner Straße 42。多くのCRMは、UK/US規則用に構築されたため、{number} {street}フォーマットでアドレスを保存します。ドイツのアドレスが低い信頼度でジオコードされている場合は、提出前にクエリ文字列の番号と通り名を逆にしてみてください。

フランス、アロンディスマン。 パリのアドレスには、郵便番号の一部として下位区(1番目~20番目)が含まれています:7500175020。アロンディスマンを省略してParisを使用するだけのクエリは、都市の中心にジオコードされます。特定の地区ではなく、これはaddressマッチではなく、低い信頼度のlocalityマッチとして表示されます。

オランダ、郵便番号の形式。 オランダの郵便番号は厳密なDDDD LLパターンに従います(4桁、スペース、2つの大文字)。スペースのないコード(1012 LGの代わりに1012LG)または小文字で保存されたコードはジオコードが正しく行われますが、郵便番号の形式を別に検証している場合は、スペースで大文字に正規化します。

ベルギー、言語のあいまい性。 ベルギーの自治体の一部は、異なるフランス語とオランダ語の名前を持っています(Liège/Luik、Gent/Gand)。APIはその両方を処理しますが、データセット内の矛盾した名前付け(一部のレコードがフランス語を使用し、一部がオランダ語を使用)は異なる信頼度を生成する可能性があります。ジオコーディングの前に、地域ごとに1つの言語に正規化します。

スペインとイタリア、通りのプレフィックスの変動。 「Calle」、「Carrer」、「Via」、「Viale」はすべて有効な通り型プレフィックスであり、レコード内で省略表記(「C/」)で表示される場合があります。ジオコーダーは一般的な省略形を処理しますが、レガシーシステムからの一般的でない短縮は正規化のステップが必要な場合があります。

同時リクエストでスループットを向上させる

順序付きスクリプトは安全でシンプルですが遅いです。より大きなデータセットでは、並行実行プログラムで順番のループを置き換えてください:

from concurrent.futures import ThreadPoolExecutor, as_completed

def main_concurrent(max_workers=10):
    df = pd.read_csv(INPUT_CSV, dtype=str).fillna('')
    # ... (以前と同じセットアップ) ...

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

    # ... (以前と同じ出力書き込み) ...

ワーカーあたり5 RPSで10個の並行ワーカーを使用すると、スループットは約50リクエスト/秒に達し、4分以内に10,000アドレスに達します。max_workersを増やす前に、MapAtlasプランの同時リクエスト制限を確認してください。

アドレス検証を繰り返し操作として実行し、毎週新しいCRMインポートをクリーニングしたり、毎晩配達アドレスを検証したりしている組織は、物流と配達ソリューションページを参照して、MapAtlas統合が運用ワークフローにどのように適合するかを確認してください。

最初の場所に不正なデータがシステムに入るのを防ぐためにアドレスオートコンプリートを構築している場合(バッチのクリーニングではなく、ソースでエラーをキャッチする)、アドレスオートコンプリートAPI:1つのフィールドがチェックアウトコンバージョンを35%向上させる方法は、フロントエンドの実装をカバーしています。

座標出力での作業

検証された出力には、受け入れられたすべてのアドレスのgeocoded_latgeocoded_lngが含まれています。これらの座標は、生のアドレス文字列では不可能な分析機能を開きます:

  • 距離計算。 倉庫と各配達アドレスの間の直線距離を計算して、配送コスト層を推定します。
  • 商圏分析。 マップ上で検証されたカスタマーの位置をプロットして、需要がどこで地理的に集中しているかを確認します。
  • 配達ゾーン割り当て。 座標がゾーンポリゴン内に落ちるかどうかをテストすることで、各アドレスを配達ゾーンに割り当てます。
  • 重複検出。 同じ座標(数メートル以内)を持つ2つのレコードは、アドレス文字列がフォーマットが異なっていても、おそらく重複しています。

NAP(名前/アドレス/電話)の一貫性とAI検索可視性への影響(特にローカルビジネスアドレスデータベースに関連)については、AI検索のNAP一貫性:不一致のアドレスがChatGPTの可視性を殺す理由は、ジオコーディング検証アドレスが構造化データマークアップの正しい基礎である理由を説明しています。

まとめ

MapAtlas ジオコーディングAPIでのバルクジオコーディングは、以下を提供します:

  • 検証されたアドレス信頼度スコア付きなので、どのレコードを自動的に信頼するか、どのレコードをレビューする必要があるかを知ることができます。
  • 構造化されたコンポーネント(通り、ハウス番号、郵便番号、都市、国)は、任意の入力形式から正規化されます。
  • 地理的座標受け入れられたすべてのアドレスについて、空間分析とルーティングを有効にします。
  • EUアドレスフォーマットサポートAPIに組み込まれており、前処理で処理する必要はありません。

Pythonスクリプトは、順序付きで5アドレス/秒で実行され、並行ワーカーで最大50/秒です。10,000アドレスでは、同時実行に応じて4~40分を予算としてください。出力は、受け入れ、レビュー、拒否の3つの層を持つクリーンなCSVです。


無料のMapAtlas APIキーにサインアップして開始してください。ジオコーディングAPIはすべてのプランでバルククエリをサポートしています。価格で、1リクエストあたりのレート、月額無料層の制限を参照してください。

役に立ちましたか?シェアしてください。

著者について

Brent van der Heiden

著者

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.

すべての記事を見る
ブログに戻る