🌊

住所正規化のデモ機能を作ったので、日本のヤバい住所を入力してみた

2023/09/11に公開

はじめに

数か月ほど前、住所の正規化が話題になりました。こちらの記事が特に有名ですね。

https://note.com/inuro/n/n7ec7cf15cf9c

関連して、こちらの記事も話題になりました。

https://yoshinorinie.hatenablog.com/entry/2023/06/09/091804

当時はほかにも色々な人が日本のヤバい住所の例をあげてくれて、とても楽しかったです。

実は弊社でもAddressianという住所正規化サービスを提供しています。初めて目にする変わった住所を見かけたら、とりあえず自社のAPIに投げてみて「おお、正規化できた」「すごい!」などといいながら遊んで働いています。

https://addressian.netlify.app/

サービスは無料で利用できますが、今までは利用の手順が面倒でした。

  1. ユーザー登録する
  2. APIキーを発行する
  3. 住所正規化APIを呼び出すプログラムを用意する(サンプルコードあり)
  4. プログラムを実行して住所を正規化する

そこで、もっと気軽に住所正規化を試してもらえるように、ユーザー登録しなくても使えるデモ機能を作ってみました。

デモ機能の概要

住所正規化デモ画面
住所正規化デモ画面

こちらが今回作った住所正規化デモ画面です。ランディングページを少しスクロールした箇所に配置しています。

住所正規化結果
住所正規化結果

正規化したい住所を入力して「正規化」ボタンをクリックするか、Enterキーを押すと、住所正規化の結果が表示されます。正規化後住所のタイプは、用途に応じて4種類用意しています。ユーザー登録せずに試せるようになったので、敷居はだいぶ下がったと思います。

デモ機能の実装

バックエンドの実装

バックエンドはAWSのChaliceで実装しました。ChaliceはAPI GatewayとLambdaおよびIAMをまとめて管理できるフレームワークです。Chaliceのコードは下記の通りです。

from chalice import Chalice, CORSConfig
import os
import requests
import json

app = Chalice(app_name='addressian_demo')
api_key = os.environ.get('API_KEY')
api_url = os.environ.get('API_URL')
allow_origin = os.environ.get('ALLOW_ORIGIN')
headers = {
    'x-api-key': api_key,
    'Content-Type': 'application/json'
}

cors_config = CORSConfig(
    allow_origin=allow_origin,
    allow_headers=['Content-Type'],
    max_age=600,
    expose_headers=[],
    allow_credentials=True
)

@app.route('/demo', methods=['POST'], cors=cors_config)
def get_normalized_address():
    address = app.current_request.json_body['address']
    data = [{'address': address}]
    res = requests.post(api_url, headers=headers, json=data)
    ret = res.json()
    return json.dumps(ret, ensure_ascii=False, indent=2)

既存の住所正規化APIをラップして、CORSの設定を追加しただけです。ChaliceのCORSはChatGPTに教えてもらいながら初めて書きました。Chaliceでは、このようにコード内でCORSの設定をするのが一般的だそうです。

フロントエンドの実装

まずはAPIを呼び出すコンポーネントを作ります。

import { FC, useState, useEffect } from 'react';
import ky from 'ky';

export type DemoProps = {
  address: string;
};

type AddressInfo = {
  type1: string;
  type2: string;
  type3: string;
  type4: string;
  zipcode: string;
  building: string;
};

type RespItem = {
  normalized_address_type1: string;
  normalized_address_type2: string;
  normalized_address_type3: string;
  normalized_address_type4: string;
  zip_code: string;
  building: string;
};

type Resp = {
  items: RespItem[];
};

const Demo: FC<DemoProps> = ({ address }) => {
  const [result, setResult] = useState<string>('');
  const [addressInfo, setAddressInfo] = useState<AddressInfo | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<boolean>(false);

  useEffect(() => {
    const normalizeAddress = async () => {
      const url = `${import.meta.env.VITE_DEMO_API_URL}`;
      try {
        const res: Resp = await ky.post(url, { json: { address } }).json();
        setResult(JSON.stringify(res, null, 2));
        setAddressInfo({
          type1: res.items[0].normalized_address_type1,
          type2: res.items[0].normalized_address_type2,
          type3: res.items[0].normalized_address_type3,
          type4: res.items[0].normalized_address_type4,
          zipcode: res.items[0].zip_code,
          building: res.items[0].building,
        });
      } catch (e) {
        setError(true);
      } finally {
        setLoading(false);
      }
    };
    void normalizeAddress();
  }, [address]);

  if (loading) {
    return (
      <div className="flex justify-center">
        <div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
      </div>
    );
  }

  if (error) {
    return <div>エラーが発生しました。</div>;
  }

  return (
    <section id="result">
      <h2>住所正規化結果</h2>
      {addressInfo && (
        <ul className="m-2">
          <li>正規化タイプ1: {addressInfo.type1}</li>
          <li>正規化タイプ2: {addressInfo.type2}</li>
          <li>正規化タイプ3: {addressInfo.type3}</li>
          <li>正規化タイプ4: {addressInfo.type4}</li>
          <li>郵便番号: {addressInfo.zipcode}</li>
          <li>建物名: {addressInfo.building}</li>
        </ul>
      )}
      <div>レスポンスJSON: </div>
      <div className="flex items-center justify-between sm:col-span-2">
        <pre className="h-64 w-full overflow-auto border border-gray-800 bg-gray-200">
          {result}
        </pre>
      </div>
    </section>
  );
};

export default Demo;

次に、住所を入力するコンポーネントを作ります。

import { FC, useState } from 'react';
import Demo from './Demo';

const DemoContent: FC = () => {
  const [address, setAddress] = useState<string>('');
  const [showDemo, setShowDemo] = useState<boolean>(false);

  const [errorMessage, setErrorMessage] = useState<string>('');

  const handleNormalize = (event?: React.FormEvent) => {
    event?.preventDefault(); // デフォルトのサブミット動作をキャンセル
    if (address.trim()) {
      setShowDemo(true);
      setErrorMessage('');
    } else {
      setErrorMessage('住所を入力してください。');
    }
  };

  return (
    <section id="Demo" className="body-font bg-gray-100 text-gray-600">
      <div className="container mx-auto px-5 py-24">
        <h1 className="title-font mb-20 text-center text-2xl font-medium text-gray-900 sm:text-3xl">
          住所正規化デモ
        </h1>
        <div className="mx-auto mb-5 grid max-w-screen-md gap-4">
          <p className="sm:col-span-2">
            住所を入力して「正規化」ボタンをクリックしてください。住所の正規化をお試しいただけます。
          </p>
          <form className="sm:col-span-2" onSubmit={handleNormalize}>
            <label
              htmlFor="address"
              className="text-sm text-gray-800 sm:text-base"
            >
              住所
              <input
                name="address"
                type="text"
                placeholder="港区芝大門1-10-18-3F"
                className="w-full rounded border bg-gray-50 px-3 py-2 text-gray-800 outline-none ring-indigo-300 transition duration-100 focus:ring"
                onChange={(e) => {
                  setAddress(e.target.value);
                  setErrorMessage('');
                  setShowDemo(false);
                }}
              />
              {errorMessage && (
                <div className="text-red-500">{errorMessage}</div>
              )}
            </label>
            <div className="flex items-center justify-between">
              <button
                type="submit"
                className="mt-2 inline-flex rounded border-0 bg-blue-500 py-3 px-6 text-lg text-white hover:bg-blue-600 focus:outline-none active:bg-blue-700"
                aria-label="住所を正規化する"
              >
                正規化
              </button>
            </div>
          </form>
          {showDemo && <Demo address={address} />}
        </div>
      </div>
    </section>
  );
};

export default DemoContent;

以上が実装の概要です。APIを呼び出して結果を表示するだけなので、シンプルな実装になっています。なお本サービスのフロントエンドとバックエンド(住所正規化ロジックは除く)の構成については、こちらの本で詳しく説明しています。

ヤバい住所をいろいろ試してみよう

それではいろいろなヤバい住所を入力して、正規化できるか試してみましょう。まずは冒頭で紹介したこちらの記事に載っていた住所から試していきます。なお、Addresianでは、国土数値情報ダウンロードサイト郵便番号データダウンロードから取得したデータを組み合わせて独自の住所データを作成し、死ぬほど複雑な高度な検索ロジックを用いて、なるべく正しく正規化できるようにしています。

いきなりラスボス 舞浜2-11

浦安市舞浜には、丁目のある住居表示実施地域と地番のみの住居表示未実施地域が混在しています。たとえば住居表示実施地域にある舞浜小学校の住所は「浦安市舞浜二丁目1番1号」ですが、住居表示未実施地域にあるディズニーアンバサダーホテルの住所は「浦安市舞浜2番地11」です。これをアラビア数字とハイフンで表すと、それぞれ「浦安市舞浜2-1-1」および「浦安市舞浜2-11」になります。どちらも「浦安市舞浜2-」で始まっているのに、かたや二丁目、かたや2番地というのはひどいですね。ちなみに舞浜二丁目11番という街区も実在します。

結論から言うと、「浦安市舞浜2-11」を一意に正規化するのは不可能です。「舞浜2番地11」と「舞浜二丁目11番」という住所がどちらも存在するため、「浦安市舞浜2-11」と書かれたら、人間でもどちらなのか判断できません。「舞浜二丁目11番」の方は、後ろに住居番号が付くはずだからその有無で判別できるのでは、と思われるかもしれません。では「浦安市舞浜2-11-101」は必ず「舞浜二丁目11番101号」(この住所は実際には存在しません)だと判断してよいでしょうか。残念ながら、そうとは限りません。「舞浜2番地11」の101号室を表している可能性があるからです。

というわけで、「浦安市舞浜2-11」は二丁目11番なのか、2番地11なのかという問題に正解はありません。Addressianでは今のところ、「浦安市舞浜2-11」を舞浜二丁目11だと判断するロジックを採用しています。

「浦安市舞浜2-11」の正規化結果
「浦安市舞浜2-11」の正規化結果

市区町村の下に丁目が来るパターン

「静岡県下田市2丁目4-26」や「埼玉県春日部市八丁目353」のように、市区町村の下にいきなり丁目が来るパターンもあります。このパターンは比較的簡単に正規化できます。

「下田市2丁目4-26」の正規化結果
「下田市2丁目4-26」の正規化結果

「春日部市八丁目353」の正規化結果
「春日部市八丁目353」の正規化結果

元記事によると、春日部市八丁目の「八丁目」は厳密には大字なので、「8丁目」と表記するのは正しくないそうです。Addressianは4種類の正規化タイプを出力しますが、タイプ1は国土交通省のデータ(下図)に記載された通りの形式で出力します。

国土交通省のダウンロードデータより
国土交通省のダウンロードデータより

ただし、この「八丁目」が大字なのか普通の丁目表記なのかは、ダウンロードデータからは読み取れません。Addressianでは普通の丁目表記と同様に扱っています。

市の下に番地が来るパターン

「奈良県御所市1番地の3」のように、市の下にいきなり番地が来るパターンもあります。「御所市1番地の3」では「1」が番地だと簡単に判断できるので、「御所市1-3」でも正規化できるか試してみましょう。

「御所市1-3」の正規化結果
「御所市1-3」の正規化結果

ban1go3なので、正しく正規化できていることがわかります。舞浜のように混在さえしていなければ、このパターンは一意に正規化可能です。

数字以外の街区符号があるパターン

次は「大阪市中央区上町A-12」や「大阪市中央区久太郎町4丁目渡辺3」のように、番地部分に数字以外の街区符号が来るパターンです。

「大阪市中央区上町A-12」の正規化結果
「大阪市中央区上町A-12」の正規化結果

街区符号のAやその下の12号も正しく正規化できています。

「大阪市中央区久太郎町4丁目渡辺3」の正規化結果
「大阪市中央区久太郎町4丁目渡辺3」の正規化結果

こちらも同様に、街区符号の渡辺と3号を正しく正規化できています。

元記事では「八街市八街は18番地2」のように、番地の前に「いろは」が来るパターンも紹介されていましたが、この住所は実は街区符号としては普通です。

国土交通省のダウンロードデータより
国土交通省のダウンロードデータより

このように「八街は」までが大字で、街区符号はアラビア数字の「18」です。

「八街市八街は18-2」の正規化結果
「八街市八街は18-2」の正規化結果

正規化するとban18になっていますね。

今度は似たような形式の住所、「羽咋市柳田町ほ79−1」の街区符号を確認してみましょう。

国土交通省のダウンロードデータより

「柳田町」までが大字で、街区符号は「ほ79」です。油断できませんね。しかも、ひとつ下の行にある「釜屋町ノ21」の「ノ」は小字・通称名に入れられているため、街区符号はアラビア数字のみの「21」です。せめて統一してほしいです。

「羽咋市柳田町ほ79−1」の正規化結果
「羽咋市柳田町ほ79−1」の正規化結果

正規化すると、今度はbanほ79になっています。石川県はほかにも特殊な街区符号の宝庫です。次に示す石川県羽咋郡志賀町のダウンロードデータをご覧ください。

国土交通省のダウンロードデータより
国土交通省のダウンロードデータより

数字以外の街区符号が430件もあります。ひらがな、全角カタカナ、半角カタカナ、漢字が全て使われて入り混じっている上に、ダウンロードデータ内でも表記ゆれしています。「エ21乙」のように半角カタカナ+数字+漢字でできた街区符号までありますね。更に恐ろしいことに、実在する住所がダウンロードデータに載っていないこともあります。

「石川県羽咋郡志賀町高浜町マ17−7」の正規化結果
「石川県羽咋郡志賀町高浜町マ17−7」の正規化結果

高浜町マ17-7は国土交通省のダウンロードデータに載っていて、正規化もできます。しかし、その近所にある高浜交番の住所「高浜町ケ1−8」は、公共施設にも関わらずダウンロードデータには載っていません。

国土交通省のダウンロードデータより
国土交通省のダウンロードデータより

「ケ8」や「ケ62」はありますが、「ケ1」は載っていませんね。このように国土交通省のデータであっても完全に信用できるわけではありません。

「石川県羽咋郡志賀町高浜町ケ1−8」の正規化結果
「石川県羽咋郡志賀町高浜町ケ1−8」の正規化結果

それでも正規化はできます。ダウンロードデータに「ケ8」があるなら、「ケ」プラス数字というパターンの街区符号がおそらくほかにも存在するだろうという仮説に基づいて、正規化ロジックを作っているためです。

長野県が2回登場するパターン

「長野県長野市南長野県町477-1」のように、住所の中に「長野県」という文字列が2回登場するパターンです。これは問題なく正規化できます。

「長野県長野市南長野県町477-1」の正規化結果
「長野県長野市南長野県町477-1」の正規化結果

「南長野」が「大字南長野」に変換されていますが、これは国土交通省のダウンロードデータに基づいています。

国土交通省のダウンロードデータより
国土交通省のダウンロードデータより

国土交通省の定めた正式な住所は「大字南長野」ですが、「大字」は省略されることも多いため、住所正規化ロジックでは「大字」ありと「大字」なしのどちらのパターンにも対応する必要があります。なお、郵便番号データ(下図)の該当箇所には「大字」なしの住所しか載っていません。

郵便番号データより
郵便番号データより

Addressianはどちらのデータも使っていますが、表記に差がある場合は国土交通省のデータを優先しています。

建物名の分離

「渋谷区道玄坂2-6-2」には藤山恒産道玄坂ビルと美奈津ビルという別々の建物が存在します。住所を正規化する目的にもよりますが、ひとつの住所に複数のビルがあるのは、あまり問題にはならない気がします。むしろ、住所と建物名を正しく分離できることが重要だと考えています。

「渋谷区道玄坂2-6-2藤山恒産道玄坂ビル地下1階」の正規化結果
「渋谷区道玄坂2-6-2藤山恒産道玄坂ビル地下1階」の正規化結果

丁目、街区符号、住居番号がフルでそろっている場合は、建物名を分離するのは比較的簡単です。もう少し難しいパターンもやってみましょう。

「鹿児島市中央町1-2-101」の正規化結果
「鹿児島市中央町1-2-101」の正規化結果

鹿児島市中央町に一丁目は存在しないため、「鹿児島市中央町1-2-101」は「鹿児島市中央町1番2号」の101号室だと判断できます。「101」を建物名として正しく分離できていますね。ルールベースの住所正規化では、このような処理は不可能です。

その他のパターン

元記事で紹介された住所以外にも、ヤバい住所はいろいろあります。いくつか試してましょう。

京都の通り名

「京都市東山区大和大路通り正面下ル大和大路1-533-3」の正規化結果
「京都市東山区大和大路通り正面下ル大和大路1-533-3」の正規化結果

住所正規化界隈では有名な京都の通り名ですね。これは正しく正規化できています。京都の通り名でやっかいなのは、書き間違いや中途半端な省略が多い上に、長すぎてどこが間違っているのか気づきにくいことです。たとえば、次の住所には間違いがあります。どこが間違いか分かりますか?

京都府東山区大和大路通り正面下る大和大路1丁目533-3

クリックして正解を表示

正解は「京都府京都市東山区」の京都市が抜けていることでした。市区町村を省略されてしまうと、正規化はできません。

大字と丁目の表記ゆれ

「埼玉県上尾市壱丁目51−2」の正規化結果
「埼玉県上尾市壱丁目51−2」の正規化結果

大字が「壱丁目」です。これは正しく正規化できていますが、「壱丁目」を「一丁目」と書かれてしまうと、残念ながら正規化できません。

「埼玉県上尾市一丁目51−2」の正規化結果
「埼玉県上尾市一丁目51−2」の正規化結果

該当する大字や丁目が見つからなかったため、town_type1が空になっています。どこまでを表記ゆれとみなして対応するかは難しい問題で、Addressianでは今のところ「一丁目」から「壱丁目」への変換はしていません。

同一名称の市

住所は都道府県を省略しても正規化できます。「府中市宮西町2-24」と「府中市府川町315」を正規化してみましょう。どちらも府中市役所の住所なのですが、都道府県が異なります。

「府中市宮西町2-24」の正規化結果
「府中市宮西町2-24」の正規化結果

こちらは東京都の府中市役所の住所です。

「府中市府川町315」の正規化結果
「府中市府川町315」の正規化結果

こちらは広島県の府中市役所の住所です。同一名称の市は少なくて、ほかに伊達市(北海道、福島県)が存在するのみです。このように、都道府県が省略されていても、市区町村から大字・丁目まで含まれていれば正規化は可能です。

読みが同じでも漢字が違う

熊本県の葦北郡芦北町大字芦北(あしきたぐんあしきたまちおおあざあしきた)には、「葦北」がひとつ、「芦北」がふたつ登場します。ひとつの住所の中に異体字の「葦」と「芦」を書かないと正しい住所にならないため、書き間違いが非常に多い住所です。「葦北郡葦北町大字葦北」や「芦北郡芦北町芦北」は厳密には正しい住所ではありませんが、間違えてしまう気持は良く分かります。正規化できるか試してみましょう。

「熊本県葦北郡葦北町大字葦北2670」の正規化結果
「熊本県葦北郡葦北町大字葦北2670」の正規化結果

「芦北郡芦北町芦北2670」の正規化結果
「芦北郡芦北町芦北2670」の正規化結果

どちらも正しく正規化できています。福岡県の糟屋郡粕屋町(かすやぐんかすやまち)や鹿児島県の肝属郡肝付町(きもつきぐんきもつきちょう)など、同様のケースはほかにもあります。「属」と「付」なんて異体字ですらありません。これらのケースは汎用的な対策が難しいため、変換テーブルを用意して地道に1件ずつ対応する必要があります。

丁目? いいえ丁です

「堺市堺区協和町5丁479-2」の正規化結果
「堺市堺区協和町5丁479-2」の正規化結果

堺市には「丁目」ではなく「丁」と表記する住所が存在します。次は同じ住所をアラビア数字とハイフンのみで入力してみましょう。

「堺市堺区協和町5-479-2」の正規化結果
「堺市堺区協和町5-479-2」の正規化結果

アラビア数字とハイフンのみで入力しても正しく「丁」に正規化できています。このように、入力値に表記ゆれがあっても同じ結果を出力できるのが、住所正規化サービスを利用するメリットのひとつです。

番のかわりに街区

こちらは宮崎県都城市の市役所のホームページの一部です。

「宮崎県都城市市役所のホームページより」
宮崎県都城市市役所のホームページより

住所をよく見ると「都城市姫城町6街区21号」となっています。都城市ではこのように「番」のかわりに「街区」が使われます。対応する国土交通省のダウンロードデータは次の通りです。

「国土交通省のダウンロードデータより」
国土交通省のダウンロードデータより

「街区符号・地番」のところにはアラビア数字が入っているだけなので、都城市で「街区」表記を使用していることは読み取れません。そのため「都城市姫城町6街区21号」を正規化すると、ほかの市区町村と同様に「6街区」は「6番」として正規化されます。また「都城市姫城町6-21」を正規化しても「都城市姫城町六街区二一号」は出力されません。

「都城市姫城町6街区21号」の正規化結果
「都城市姫城町6街区21号」の正規化結果

北海道の条

北海道にも特殊な住所表記がいくつかあります。まずは「条」です。

「札幌市中央区南二条西一丁目5丸大ビル2階」の正規化結果
「札幌市中央区南二条西一丁目5丸大ビル2階」の正規化結果

「南二条西一丁目」までが大字・丁目にあたります。同じ住所を表記ゆれさせてみましょう。

「札幌市中央区南2条西1ー5ー2F」の正規化結果
「札幌市中央区南2条西1ー5ー2F」の正規化結果

建物名の分離も含めて正しく正規化できました。

北海道の線 さらに小字が数値プラス号

北海道には「条」のほかに「線」もあります。これはイレギュラーな街区符号のパターンを探していたときに偶然見つけた住所ですが、なかなか強烈です。

「国土交通省のダウンロードデータより」
国土交通省のダウンロードデータより

「西神楽一線」が大字・丁目です。そして、なんと小字が「五号」です。したがって「旭川市西神楽1線5号67番90号」のように、「5号」の後ろに更に地番が続く、住所正規化ロジックにとっては悪夢のような住所が生まれます。

「旭川市西神楽1線5号67番90号」の正規化結果
「旭川市西神楽1線5号67番90号」の正規化結果

これは実在する児童公園の住所です。「5号」より後ろの部分を建物名だと誤判断することもなく、正常に正規化できました。今度は全部漢数字で入力してみましょう。

「旭川市西神楽一線五号六十七番九十号」の正規化結果
「旭川市西神楽一線五号六十七番九十号」の正規化結果

ひとつの住所の中に「五号」および「九十号」というふたつの「号」が登場するにも関わらず、こちらも正しく正規化できました。この住所をルールベースのプログラムで正規化するのは、相当難しいでしょう。

まとめ

日本にはヤバい住所が多すぎて、とても長い記事になってしまいました。最後まで読んでいただき、ありがとうございます。今回の修正で気軽に住所正規化を試せるようになったので、皆様もぜひAddressianにいろいろな住所を入力してみてください。もし、正規化できない住所を見つけたら、教えていただけるとありがたいです。Addressianの正規化ロジックを改善するための、参考にさせていただきます。

Discussion