住所をそのままAPIに投げると、意外な確率で結果が空になる
住所リストから郵便番号を引きたいとき、手っ取り早いのは住所の文字列をそのままGeocoding APIに投げてしまうことです。ところが実際にやってみると、resultsが空配列で返ってくるケースが思った以上に多いことに気づきます。
原因のほとんどは、住所の末尾に建物名・部屋番号・会社名などが付いていることです。「東京都〇〇区1-2-3 〇〇マンション101」のような文字列は、番地までは問題なくジオコーディングできるのに、その後ろの建物名部分が地名として解釈できずノイズになり、全体としての検索結果がヒットしなくなります。1件2件ならログを見ながら手で直せますが、数百件・数千件の住所リストを一括処理するバッチ処理では、これを自動で吸収する仕組みが必要になります。そこで、失敗した場合に住所を段階的に加工して再検索する、多段フォールバック方式のジオコーディング処理を作りました。
対策1: 番地より後ろの建物名らしき部分を切り落とす
一段目のフォールバックとして、住所の中から「数字の直後にある区切り」を境目とみなし、それより前の部分だけを使って再検索します。
def strip_building_name(address)
if address =~ /\d\s/
address.match(/(^.+\d)\s/)[1]
elsif address =~ /\d\n/
address.match(/([\s\S]*\d)\n/)[1].gsub("\n", "")
else
address
end
end
番地(数字)の直後にスペースや改行がある、という素朴なパターンマッチですが、実務の住所データではこれだけでもかなりの割合の「建物名ノイズ」を除去できます。厳密に住所の文法を解析しているわけではないので取りこぼしはありますが、多段フォールバックの1段目としては十分な精度です。
対策2: 全角スペース→半角スペース区切りの順で段階的に再試行する
建物名を除去してもなお結果が空、あるいは住所の構成要素(address_components)が取得できない場合は、住所を区切り文字で分割して前半部分だけを使う、というリトライをさらに重ねます。全角スペースと半角スペースが住所データ内で混在しがちなので、両方を順番に試すのがポイントです。
def geocode_with_fallback(client, address)
candidates = [
address,
strip_building_name(address),
address.split(" ").first, # 全角スペース区切り
address.split(" ").first, # 半角スペース区切り
].compact.uniq
candidates.each do |candidate|
result = client.geocode(candidate)
return result if result && !result_empty?(result)
end
nil
end
def result_empty?(result)
results = result["results"]
results.nil? || results.empty? || results[0]["address_components"].nil?
end
候補を配列にまとめてuniqで重複を除いてから順番に試す形にしておくと、「建物名除去した結果がたまたま元の住所と同じだった」ような無駄なAPI呼び出しを避けられます。API呼び出し回数はそのまま課金やレート制限に直結するので、候補の重複排除は地味に効いてきます。候補の順序も「情報量が多く精度が高いものから、加工の度合いが大きく粗いものへ」という並びにしているので、可能な限り正確な結果を優先しつつ、最終的には粗い候補まで試し切ることで取得率を上げています。
対策3: 郵便番号のフォーマットチェックで「それらしい誤答」も弾く
APIが結果を返してきても、住所解析の精度によっては郵便番号として不正な値が混ざることがあります。最後に正規表現でフォーマットをチェックし、通らなければ失敗扱いにします。
def extract_postalcode(result)
postalcode = result["results"][0]["address_components"][-1]["short_name"]
return nil unless postalcode.match?(/\d{3}-\d{4}/)
postalcode
end
resultsが空でなければ成功、というだけで判定を終わらせてしまうと、番地を都道府県レベルまで広く解釈した結果、実際の建物とはかけ離れた郵便番号を「取得できた」と誤認することがあります。フォーマットチェックを最後に挟んでおくことで、こうした「それらしいが間違っている」結果を弾き、後続の処理に誤ったデータを流さないようにしています。
複数データソースをまたいだ重複排除は共通クラスに切り出す
住所リストを複数のサイトから集めていると、同じ施設・同じ住所が別のデータソースに重複して登場することがあります。この判定を呼び出し側で個別に書くと、抽出元が増えるたびに同じifがあちこちに増えてしまうため、独立したクラスに切り出しておきます。
class DuplicateChecker
def self.duplicate?(name, address, name_history, address_history)
name_history.include?(name) || address_history.include?(address)
end
end
判定ロジック自体は「名前一致 or 住所一致のどちらかで重複とみなす」という単純なものですが、これを1メソッドに集約しておくことが重要です。抽出元のスクリプトごとに同じ判定を書いていると、後から「住所だけでなく名前の表記ゆれも見たい」といった基準の変更が入ったときに、修正漏れが起きて一部のスクリプトだけ古い基準のまま動き続ける、という不整合が発生しがちです。
呼び出し側の全体像
これらを組み合わせると、呼び出し側の処理は次のようにシンプルになります。
name_history = load_name_history
address_history = load_address_history
list.each do |name, raw_address|
next if DuplicateChecker.duplicate?(name, raw_address, name_history, address_history)
result = geocode_with_fallback(geocoding_client, raw_address)
next if result.nil?
postalcode = extract_postalcode(result)
next if postalcode.nil?
save_record(name, raw_address, postalcode)
end
重複排除・多段フォールバック・フォーマットチェックという3つの関心事がそれぞれ独立した関数・クラスに分かれているので、呼び出し側は「重複ならスキップ、ジオコーディングに失敗したらスキップ、フォーマットが不正ならスキップ」という条件を並べるだけで済みます。なお、APIキーはコード中に埋め込まず、環境変数から読み込むようにしています。
api_key = ENV.fetch("GEOCODING_API_KEY")
まとめ
- 住所文字列をそのままGeocoding APIに投げると、建物名ノイズによって結果が空になりやすい。番地より後ろを切り落とすフォールバックをまず用意する
- それでも空なら全角スペース→半角スペースの順で区切って前半部分だけを試す、という段階的なリトライを積む。候補は
uniqしてから試すと無駄なAPI呼び出しを避けられる - 取得した郵便番号は正規表現でフォーマットチェックし、「結果はあるが精度が粗い誤答」も弾く
- 複数データソースをまたぐ重複排除は「名前一致 or 住所一致」のような基準を独立したクラスに切り出し、抽出元が増えても判定基準を一箇所で管理できるようにする
- APIキーなどの認証情報はコードに埋め込まず、環境変数から読み込むようにしておく

コメント