国土地理院の住所検索APIでClaude抽出の住所を検証する — 緯度経度と表記ゆれ
請求書や名刺からClaudeに抜かせた住所を、国土地理院の住所検索APIで緯度経度に直して突き合わせます。誤抽出が別の町にヒットする例と、4区分の判定スクリプトを載せます。
請求書や名刺の画像から、Claudeに住所を抜き出させる作業は珍しくなくなりました。ただ、抜き出した住所が正しいかどうかは、読みやすい日本語になっているかとは別の話です。区名が一字違う、丁目が落ちる、といった誤りは、文字列として眺めても気づきにくいものです。
そこで、国土地理院の住所検索APIに住所を投げ、返ってきた緯度経度と地名を元の文字列と突き合わせます。誤抽出された住所は、別の町にヒットするか、粗い粒度で止まるかのどちらかになりやすく、その差を機械で拾えます。この記事は、APIの返り方、4区分の判定スクリプト、Claude Codeに回させる設定までを順に扱います。
国土地理院の住所検索APIで何が確かめられるか
国土地理院の住所検索APIは、住所の文字列をクエリに渡すと、該当する地点の緯度経度と地名をJSONで返すWebエンドポイントです。APIキーの登録は要りません。ブラウザでも次のURLを開くだけで結果が見えます。
curl -s -G "https://msearch.gsi.go.jp/address-search/AddressSearch" \
--data-urlencode "q=東京都千代田区千代田1-1"返ってくるのは、GeoJSONのFeatureが並んだ配列です。実際に叩いた結果は次の形でした。
[{"geometry":{"coordinates":[139.754089,35.683659],"type":"Point"},
"type":"Feature",
"properties":{"addressCode":"","title":"東京都千代田区千代田1番"}}]読み取る点は3つあります。
coordinatesの順番は経度、緯度です。地図アプリの「緯度, 経度」とは逆になりますtitleに、APIが解釈した住所が入ります。入力と同じとは限りません- 該当がなければ、エラーでなく空配列
[]が返ります
この title こそが検証の材料です。Claudeが抽出した住所を入力に使い、title を見れば、APIがその住所をどこまで、どの町として解釈したかが分かります。
仕様書の保証ではなく、叩いて確かめた挙動
この記事に書く挙動は、すべて実際にリクエストして確かめた結果です。仕様書で保証された動作として書いてはいません。参考になる記述は、地理院地図のソースを公開しているリポジトリのREADMEにあります。そこには、サーバー側の動的機能(地名検索機能等)は常に、また長期的に提供できるとは限らず、仕様や利用方法は予告なく変更する場合がある、と書かれています。
同種の機能であるこのAPIも、同じ前提で扱うのが安全です。返り値の形が変わっても検証が黙って通り続けないよう、想定外の形では例外で止まる書き方にしています。
なぜ座標で突き合わせると誤抽出が見えるのか
住所の誤りには、文字列の差が小さいのに地点が大きくずれるものがあります。逆に、表記は大きく違うのに、同じ地点を指すものもあります。文字列の比較だけでは、前者を見逃し、後者を誤りと判定します。
ジオコーダを挟むと、この二つを分けて扱えます。
| 入力の状態 | APIの返り方 | 検証での扱い |
|---|---|---|
| 表記が違うだけで正しい | APIの返り方同じ地名・同じ座標 | 検証での扱い合格 |
| 町名が誤っている | APIの返り方別の町の座標 | 検証での扱い不一致 |
| 番地や丁目が不明 | APIの返り方区や町までの座標 | 検証での扱い粒度不足 |
| 実在しない住所 | APIの返り方空配列 | 検証での扱い該当なし |
入力の漢数字と算用数字、「1-1」と「1番1号」の違いは、APIが吸収してくれます。たとえば「新宿区西新宿2-8-1」と「新宿区の西新宿二丁目8番1号」は、どちらも同じ座標(139.691772, 35.689629)に解決されました。表記ゆれの吸収をAPI側に任せ、こちらは「返ってきた地名が入力の町と合っているか」だけを見る設計にできます。
APIの返り方を5パターンで確かめる
判定ロジックを書く前に、入力を変えたときの返り方を確認しておきます。以下は同じエンドポイントに、住所を変えて投げた結果です。
入力 -> 返ったtitle -> 見立て
東京都千代田区千代田1-1 -> 東京都千代田区千代田1番 -> 番まで解決
東京都渋谷区神南1-19-11 -> 東京都渋谷区神南一丁目19番11号 -> 号まで解決
東京都渋谷区神南一丁目 -> 東京都渋谷区神南一丁目 -> 丁目で止まる
東京都千代田区西新宿2-8-1 -> 東京都千代田区西神田 -> 別の町にヒット
存在しない市町村ほげ -> (空配列) -> 該当なし注目したいのは4行目です。千代田区に西新宿という町はありません。区名を取り違えた入力ですが、APIは空配列を返さず、千代田区の西神田を返しました。HTTPは成功し、座標も付いてきます。件数だけを見て「ヒットしたから正しい」と判断すると、この誤りは通ってしまいます。
粒度も一定ではありません。千代田1-1は「番」まで、神南1-19-11は「号」まで返りました。同じ丁目・番・号の入力でも、解決の深さは地点によって変わります。そのため、「番号まで一致したか」を厳密に求めるより、「町名が合っているか」「入力にあった番号が完全に落ちていないか」を見るのが現実的です。
また、郵便番号を先頭に付けた入力は、実際に叩くと0件になりました。「〒100-0001」で始まる名刺の書式は、そのまま投げずに郵便番号を除いてから渡します。建物名や階数(「○○ビル5F」)を末尾に付けた入力は、番地までの解決が保たれました。
4区分で判定するスクリプト
確認した挙動から、結果を4つに分けます。
- OK: 返った地名が入力に含まれ、番号の粒度も保たれている
- MISMATCH: 返った町名が、入力に含まれていない(別の町にヒット)
- COARSE: 入力に番号があるのに、区や市までしか返らない
- NOT_FOUND: 空配列
Pythonの標準ライブラリだけで書いたスクリプトです。丁目・番・号と漢数字を正規化して比較します。
import json, re, sys, unicodedata, urllib.parse, urllib.request
API = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
KAN = str.maketrans("〇一二三四五六七八九", "0123456789")
def norm(s):
s = unicodedata.normalize("NFKC", s).translate(KAN)
s = re.sub(r"〒?\d{3}-?\d{4}", "", s)
s = re.sub(r"(\d)丁目", r"\1-", s)
s = re.sub(r"(\d)(番地?|番)", r"\1-", s)
s = s.replace("号", "")
s = re.sub(r"[ ‐−-―]", "-", s)
return re.sub(r"-+", "-", s).strip("-")
def strip_zip(s):
s = unicodedata.normalize("NFKC", s)
return re.sub(r"〒?\s*\d{3}[-ー−]?\d{4}\s*", "", s).strip()
def geocode(addr):
url = API + urllib.parse.quote(strip_zip(addr))
with urllib.request.urlopen(url, timeout=20) as r:
feats = json.load(r)
if not feats:
return None
lon, lat = feats[0]["geometry"]["coordinates"]
title = feats[0]["properties"]["title"]
return {"lat": lat, "lon": lon, "matched": title}
def verdict(addr, g):
if g is None:
return "NOT_FOUND"
pref = lambda x: re.sub(r"^(東京都|北海道|(?:京都|大阪)府|.{2,3}県)", "", x)
a, m = pref(norm(addr)), pref(norm(g["matched"]))
place = re.sub(r"[\d-]+$", "", m) # 返ってきた地名部分
if place not in a:
return "MISMATCH"
if re.search(r"\d", a) and not re.search(r"\d", m):
return "COARSE"
return "OK"
for line in sys.stdin:
addr = line.strip()
if addr:
g = geocode(addr)
print(verdict(addr, g), addr,
g["matched"] if g else "-", sep="\t")verify_address.py として保存し、住所を1行ずつ書いたテキストを標準入力に渡します。
python3 verify_address.py < addresses.txt手元で8件を流した結果です。
判定 -> 入力 -> 返った地名
OK -> 東京都千代田区千代田1-1 -> 東京都千代田区千代田1番
MISMATCH -> 東京都千代田区西新宿2-8-1 -> 東京都千代田区西神田
OK -> 東京都新宿区西新宿二丁目8番1号 -> 東京都新宿区西新宿二丁目8番
OK -> 〒100-0001 東京都千代田区千代田1-1 -> 東京都千代田区千代田1番
OK -> 東京都港区六本木6丁目10−1 -> 東京都港区六本木六丁目10番
OK -> 東京都中央区銀座4-6-16 ○○ビル5F -> 東京都中央区銀座四丁目6番16号
COARSE -> 東京都千代田区ほげ町1-1 -> 東京都千代田区
OK -> 北海道札幌市中央区北1条西2丁目 -> 北海道札幌市中央区北一条西二丁目ほげ町の行に注目してください。存在しない町名でも、区の中心点が返ります。空配列にはならず、座標も付くので、COARSEの区分がなければ素通りしていた入力です。
書き換えた箇所が壊れていないかを自分で確かめる
正規化の関数は、書き換えるたびに結果が変わります。最初の版では「六本木6丁目」が不一致と出ました。漢数字の変換を丁目の置換より後に置いていたため、返り値の「六本木六丁目」だけが数字にならなかったのが原因です。変換の順序を入れ替えて直しました。
この種の不具合は、上の8件のような既知の入出力表を手元に置いておけば、すぐ見つかります。Claude Codeに関数を直させるときも、この表を回帰テストに使うと安全です。なお、この正規化は十・百を含む漢数字(「二十丁目」など)を扱いません。該当する住所が混ざるデータでは、変換表を広げるか、別の手段を足してください。
Claudeに住所を抽出させ、検証まで回す
ここからは、抽出の側です。請求書や名刺のテキストから住所を取り出す部分を、Claude Codeの非対話モードで書けます。構造化出力を指定すると、結果の取り出しが安定します。
SCHEMA='{"type":"object","required":["addresses"],
"properties":{"addresses":{"type":"array",
"items":{"type":"string"}}}}'
cat invoice.txt | claude -p "請求元と請求先の住所を抽出して" \
--output-format json --json-schema "$SCHEMA" \
| jq -r '.structured_output.addresses[]' > addresses.txt公式のドキュメントによると、--output-format json と --json-schema を併用すると、スキーマに沿った結果が structured_output フィールドに入ります。上のコマンドは、その配列を1行1住所のファイルに落としています。続けて、先ほどのスクリプトを通します。
python3 verify_address.py < addresses.txt | grep -v '^OK'OK以外の行だけが出力されるので、人の目で見るのは疑わしい住所に絞られます。抽出してから公的APIで裏を取る流れは、インボイス登録番号のWeb-API照合やe-Stat APIでも同じ形になります。PDFから契約情報をまとめて抜く側は、契約台帳をPDFから一括抽出で扱っています。
CLAUDE.mdに検証の手順を残す
繰り返し使う作業なら、手順をCLAUDE.mdに書いておきます。例えば次のような断片です。
## 住所の抽出と検証
- 住所を抽出したら、必ず verify_address.py に通してから報告する
- MISMATCH と COARSE は「要確認」として一覧にし、修正案は出さない
- NOT_FOUND は元の画像テキストを読み直し、誤読の可能性を併記する
- 郵便番号は照合に使わず、住所の文字列だけで判定する修正案を出させないのは、意図があります。MISMATCHの住所に対してClaudeが「正しい住所はこちらです」と補うと、書かれていた住所を別の住所に差し替えてしまう恐れがあるからです。差し替えが妥当かどうかは、原本を見られる人が決めます。
Claude Codeに検証コマンドを実行させる反復
スクリプト自体の改善も、Claude Codeと進められます。「verify_address.pyに、手元の既知8件を入力して期待判定と一致するか確認するテストを足して。実行して結果を見せて」と頼みます。実行出力を読ませて直させる反復にすると、正規化の誤りを、報告の前に潰せます。
つまずきやすい点
- 空配列を「住所が誤り」と即断しない: 正しい住所でも該当しない可能性は排除できません。NOT_FOUNDは「要確認」であって「誤り」ではありません
- 座標は建物の位置ではない: 返るのは1点の座標で、建物の入口や敷地の中心だと確かめたわけではありません。距離で突き合わせるなら、小さな差は誤差として扱います
- 続けて大量に投げない: 利用上限の数字を前提にせず、1件ごとに間隔を空け、同じ住所は結果を保存して再利用します
- 返り値の形が変わる可能性: 前述のとおり、仕様が予告なく変わる場合があります。
coordinatesが2要素の配列、titleが文字列、という前提が崩れたときは、判定を通さず失敗として止めます
住所をAPIに送ることの意味
検証のたびに、住所の文字列は国土地理院のサーバーに送られます。請求書や名刺の住所は、法人の所在地なら公開情報で足りることが多い一方、個人の氏名と結びつく住所は個人情報に当たる場合があります。個人宛ての住所を扱うデータでは、組織のルールで外部送信が許されるかを先に確認してください。氏名や電話番号は、抽出の時点でスキーマから外しておくと、検証に流れる情報を住所だけに絞れます。
検証結果を使うときの出典
国土地理院コンテンツ利用規約は、公共データ利用規約(第1.0版)に基づいています。規約には、コンテンツを利用する際は出典を記載すること、編集・加工して利用する場合は、その旨も記載することが求められています。規約が対象にしているのは国土地理院のウェブサイトのコンテンツで、住所検索APIの返却データへの適用は、規約のページに明記がありません。適用される前提で考えるなら、検証のために座標を内部で使うだけの場面は問題になりにくく、結果を社外向けの資料や公開データに載せる場面では、出典と加工の記載が必要になります。記載例の詳細は、規約のページにあります。
まとめ
Claudeが抽出した住所の検証は、文字列の見た目でなく、ジオコーダが返した地名との整合で行うと、誤りを機械で拾えます。国土地理院の住所検索APIは、表記ゆれ(漢数字・丁目番号の書き方)を吸収し、実在しない町には区の中心を返すこともあります。そのため「ヒットしたかどうか」でなく、返った title を入力と照らす4区分の判定が要ります。
CLAUDE.mdに「抽出したら必ず検証にかける」「MISMATCHとCOARSEは直さず一覧にする」と書き、既知の入出力表をテストに使えば、抽出から検証までを毎回同じ形で回せます。仕様の保証がない点と、住所が外部に送られる点だけは、運用の前に決めておく必要があります。