【Ruby】住所を検出・分割するAPIが欲しいので作ってみた

# 概要
　住所絡みのAPIにおいて、「住所から緯度・経度を割り出すAPI」とか「郵便番号から住所を割り出すAPI」とかはよくありますが、「文字列から住所を切り出すAPI」は案外ないものですね……。
　例えば「住所：〒105-0011 東京都港区芝公園4丁目2-8東京タワー大展望台2F TEL：03-3433-5111」を「`["東京都","港区","芝公園","4-2-8","東京タワー特別展望台2階"]`」と変換するぐらい**パワフルな住所APIが欲しかった**ので、Rubyで実際に作成してみました。

**※免責事項：以下のコードは、あらゆる住所を正確に検出・分割できることを保証しません。**
※完成品だけ欲しい場合はここに置いておきます(**MITライセンス**とします)。
　　[住所分割用ライブラリ](https://gist.github.com/YSRKEN/51ceea860328ca43533d3e883d9c6078)

# Step0：必要なデータを用意する
　住所を検出する際、一番頼りになるのは**公式が出す郵便番号データ**です。
　　　[郵便番号データダウンロード - 日本郵便](http://www.post.japanpost.jp/zipcode/download.html)
　ただ、この郵便番号データには色々と問題があり、

- 列数がちょっと多すぎる(フラグ項目が6列もある)
- 1行にするべきデータが分割されている
- 「以下に掲載がない場合」「○○市一円」などの冗長な情報が記されている

といった点があることから、実際には**第三者が加工したものを用いるのがベター**とされています。
　　[郵便番号データは自分で加工しない - daily dayflower](http://d.hatena.ne.jp/dayflower/20100929/1285744153)
　　[郵便番号から住所を検索するサービスにまともなものがない - ぐるぐる～](http://bleis-tift.hatenablog.com/entry/20080531/1212217681)
　　[郵便番号データのダウンロード - zipcloud](http://zipcloud.ibsnet.co.jp)
　ここで、プログラミング言語からの扱いやすさを考慮した結果、私はSQLite形式でアップロードされているデータを採用することにしました。このサイトにはMySQLなどのDB形式もあるのですが、rubyなら`gem 'sqlite3'`すればすぐ使えるので便利……というのが採用理由です。
　　[住所データSQL【住所.jp】](http://jusyo.jp/sqlite/new.php)

# Step1：余計な文字列を削除する
　住所を検出するより先に、余計な文字列を削除してしまいましょう。つまり、**電話番号**とか**郵便番号**とか**括弧に覆われた文字列**などを置換テクニックで処理してしまうわけです。こうすることで、後の文字列処理がしやすくなります。

```ruby
# input_addressを入力とする

# NFKC正規化しておく
address = UNF::Normalizer.normalize(input_address, :nfkc)
# 電話番号と思わしき文字列を削除
address.gsub!(/[\d\(\)-]{9,}/, '')
address.gsub!(/TEL:|FAX:|TEL|FAX/, '')
# 郵便番号と思わしき文字列を削除
address.gsub!(/\d\d\d-\d\d\d\d/, '')
address.gsub!(/〒|郵便番号|郵便/, '')
# 括弧に囲われた部分を削除
address.gsub!(/【.*?】/, '')
address.gsub!(/≪.*?≫/, '')
address.gsub!(/《.*?》/, '')
address.gsub!(/◎.*?◎/, '')
address.gsub!(/〔.*?〕/, '')
address.gsub!(/\[.*?\]/, '')
address.gsub!(/<.*?>/, '')
address.gsub!(/\(.*?\)/, '')
address.gsub!(/「.*?」/, '')
# 特定フレーズの後にある文字を削除
address.gsub!(/(◎|※|☆|★|◇|◆|□|■|●|○|~|〜).*/, '')
```

# Step2：都道府県を検出する
　主に2種類の方法が考えられます。

- Step0のDBから都道府県一覧を取り出して照合する
- ざっくり正規表現でマッチングする

```ruby

# DBを読み込む
require "sqlite3"
db = SQLite3::Database.new 'zenkoku.sqlite3'
# 都道府県一覧を取り出す(Enumerable#mapで1列目だけ取り出しているのがポイント)
sql = "SELECT ken_name from ad_address GROUP BY ken_name"
pref_list = db.execute(sql).map{|p| p[0]} # ["三重県", "京都府", ..., "鹿児島県"]
# 分析開始(addressが取り出される対象で、norm_address[:pref]が検出した都道府県名)
pref_list.each{|pref|
	# 取り出した都道府県名にマッチしない場合は飛ばす
	next unless address.include?(pref)
	# マッチする場合は、「マッチ箇所以降の文字列」を取り出す
	norm_address[:pref] = pref
	address = address.match(/#{norm_address[:pref]}(.*$)/)[1]
	break
}

# DBを読み込まない場合のコード
# 当然「東京県」などの出鱈目な住所にもマッチしてしまうので注意
regex = /[^\x00-\x7F]{2,3}県|..府|東京都|北海道/
if address =~ regex
	norm_address[:pref] = address.match(regex)[0]
	address = address.match(/#{norm_address[:pref]}(.*$)/)[1]
end
```

# Step3：市区を検出する
　Step2で都道府県が判明している場合は、それをキーとしてDB検索することにより、市区一覧を取り出すことができます。
　そこで、どれにマッチするかを順に試すことにより、市区情報を検出可能となります。

```ruby
if norm_address[:pref] != ""
	# 都道府県が判明している場合、市区一覧を取り出し、そこからマッチングを行う
	sql = "SELECT city_name FROM ad_address WHERE ken_name='#{norm_address[:pref]}' GROUP BY city_name"
	city_list = db.execute(sql).map{|p| p[0]}
	# 市区を検出する
	# (既存の市区にマッチさせる)
	match_index = address.size # norm_address[:city]がマッチした位置
	city_list.each{|city|
		# cityがマッチした位置
		match_index_temp = address.index(city)
		# cityがマッチした際、
		# A. match_indexよりも手前の位置にマッチした場合
		# B. match_indexと同じ位置にマッチし、cityがnorm_address[:city]より長い場合
		# のどちらかなら、norm_address[:city]とmatch_indexを更新する
		if !match_index_temp.nil?
			if match_index_temp < match_index
				norm_address[:city] = city
				match_index = match_index_temp
			end
			if match_index_temp == match_index && city.size > norm_address[:city].size
				norm_address[:city] = city
				match_index = match_index_temp
			end
		end
	}
	address = address.match(/#{norm_address[:city]}(.*$)/)[1]
end
```

　また、Step2で都道府県が判明していない場合、上記コードから`WHERE ken_name='#{norm_address[:pref]}'`を抜いた上で検索を行うこともできますが、当然その分だけ計算コストは増大します。
(例：2018/01/18現在、「東京都」で**絞り込むと市区は62種類**しかないが、**絞り込まないと1933種類**から検索を行うことになる)
　それを嫌って単なる正規表現で処理する場合、例えばこういったコードを書くことになるでしょう。
　　参考：[短い市区町村名・長い市区町村名](https://uub.jp/zat/mijikainagai.html)

```ruby
# マッチさせるパターンを配列で用意する
regex_pattern = []
regex_pattern.push(/([^\x00-\x7F]{1,6}市[^\x00-\x7F]{1,4}区)/)
regex_pattern.push(/([^\x00-\x7F]{1,3}郡[^\x00-\x7F]{1,5}町)/)
regex_pattern.push(/(四日|廿日|野々)市市/)
regex_pattern.push(/([^\x00-\x7F市]{1,6}市)/)
regex_pattern.push(/([^\x00-\x7F]{1,4}区)/)
# 順に試していき、マッチしたものを判定結果とする
regex_pattern.each{|pattern|
	# マッチしなければ飛ばす
	next unless address =~ pattern
	norm_address[:city] = address.match(pattern)[0]
	address = address.match(/#{norm_address[:city]}(.*$)/)[1]
	break
}
```

# Step4：町村を検出する
　ここを無視して先に「Step5：番地を検出する」で**番地を検出した後に町村を割り出す**(町村は番地より手前側にあるため)こともできますが、判定精度を考えると先に判定しておいた方が無難でしょう。
　ここを正規表現で考えようとするとマッチングするパターンの量が**大変なことになりそう**ですので、SQLの結果から絞り込む作戦でいいかと思われます(方法は「Step3：市区を検出する」と同様なので略)。

# Step5：番地を検出する
　まず、「番地」がどういった文字列かについて考えてみましょう。
「数字から始まる」ことに異論の余地はないと思いますが、ここで言う数字とは**漢数字も指す**ことに注意が必要です。流石に「`壱弐参`」などといった旧字体は出てこないと思われますが、それでも「`一二三四五六七八九十百`」ぐらいまでは考慮が必要になるでしょう。
　また、数字と数字の間を繋ぐ文字列としては、正規表現で表現すると「`/丁目|丁|番地|番|号|-|‐|ー|−|の|東|西|南|北/`」ぐらいの候補があると思われます。これらの情報から、番地にマッチすると思われる正規表現は次のように表現されます。

```ruby
#漢数字
k_num = '[一二三四五六七八九十百千万]'
#繋ぎ文字1：数字と数字の間(末尾以外)
s_str1 = '(丁目|丁|番地|番|号|-|‐|ー|−|の|東|西|南|北)'
#繋ぎ文字2：数字と数字の間(末尾)
s_str2 = '(丁目|丁|番地|番|号)'
#全ての数字
all_num = "(\\d+|#{k_num}+)"
#「先頭は数字、途中は数字か繋ぎ文字1、最後は数字か繋ぎ文字2」を満たす正規表現
regex6 = /#{all_num}*(#{all_num}|#{s_str1}{1,2})*(#{all_num}|#{s_str2}{1,2})/
```

後は、それに正規表現でマッチングさせるだけで、番地情報を取り出すことができます。

```ruby
# マッチングを行う
if address =~ regex_pattern
	norm_address[:addr1] = address.match(regex_pattern)[0]
	address = address.match(/#{norm_address[:addr1]}(.*$)/)[1]
end
```

　……ただ、この方式の場合、

- 「六万寺町」「百人町」など、数字入りの町名である
-  Step4で町名を取り除いていない

の両条件を満たすと**町名の一部を番地と誤認してしまいます**。Rubyの正規表現は、たとえ最長マッチングでも**手前の位置にヒットした方が優先される**ため、対策として次のようなコードを書くことになります。
(※Step4をちゃんと実施していれば不要な苦労だと思われます)

```ruby
if address =~ regex_pattern
	# 番地候補を初期化
	addr1_list = []
	# 番地候補を追加していく
	temp_address = address.clone #sub!メソッドで消していくので生贄を立てる
	while temp_address =~ regex_pattern
		addr1 = temp_address.match(regex_pattern)[0]
		addr1_list.push(addr1)
		temp_address.sub!(addr1, "")
	end
	# 最も長い番地候補が正しい番地だと思われる
	norm_address[:addr1] = addr1_list.max{|a, b| a.size <=> b.size}
	norm_address[:town] = address[0, address.index(norm_address[:addr1])] #ついでに町村も推定
	address = address.match(/#{norm_address[:addr1]}(.*$)/)[1]
end
```

　なお、こうして取り出された番地は、「丁目」や「ー」などの英数字記号以外の文字列を含むことになります。全て「4-2-8」などの英数字記号な形に正規化したい場合のロジックも貼っておきます。

```ruby
# 番地を正規化する
def norm_addr1(addr1)
	addr1_temp = addr1.clone
	# ハイフン以外のハイフンっぽい記号を置き換える
	addr1_temp.gsub!(/-|‐|ー|−/, '-')
	# 「丁目」などをハイフンに置き換える
	addr1_temp.gsub!(/丁目|丁|番地|番|号|の/, '-')
	addr1_temp.gsub!(/-{2,}/, '-')
	addr1_temp.gsub!(/(^-)|(-$)/, '')
	# 漢数字をアラビア数字に置き換える
	pattern = /[一二三四五六七八九十百千万]+/
	while addr1_temp =~ pattern
		match_string = addr1_temp.match(pattern)[0]
		arabia_number_string = "#{kan_to_arabia(match_string)}"
		addr1_temp.sub!(match_string, arabia_number_string)
	end
	return addr1_temp
end

# 漢数字をアラビア数字に変換する
# 実は「十一万」以上の文字列で変換ミスが発生するが、
# 番地変換でそこまで大きな数を考慮することはないと思われる
def kan_to_arabia(str)
	# 変換するためのハッシュ
	hash = {
		"一" => 1, "二" => 2, "三" => 3, "四" => 4, "五" => 5, 
		"六" => 6, "七" => 7, "八" => 8, "九" => 9, "〇" => 0, 
		"十" => 10, "百" => 100, "千" => 1000, "万" => 10000
	}
	# 漢数字を数字に置き換える
	num_array = str.chars.to_a.map{|c| hash[c]}
	# 10未満の数字を横方向に繋げる
	# 例：[1,9,4,5]→[1945]
	num_array2 = []
	temp = 0
	num_array.each{|num|
		if num < 10
			temp *= 10
			temp += num
		else
			if temp != 0
				num_array2.push(temp)
			else
				num_array2.push(1)
			end
			num_array2.push(num)
			temp = 0
		end
	}
	num_array2.push(temp)
	# 10・100・1000・10000の直前にある数字とで積和する
	# 例：[2,100,5,10,3]→253
	val = 0
	0.upto(num_array2.size / 2 - 1).each{|i|
		val += num_array2[i * 2] * num_array2[i * 2 + 1]
	}
	val += num_array2.last
	return val
end
```

# Step6：建物名などを検出する
　住所だけならStep5までで取得できますが、ここまで来ると建物名も取得したくなると思います。
　番地までを取り去った文字列を「建物名」として終了……でも構いませんが、入力データというのはしばしば、

- 余計な空白文字が含まれる(番地までと建物名との間に空白を入れるなど)
- 複数の住所を並べてしまっている(余分な住所を削らないと「建物名」がとんでもないことに)
- 電話番号やメールアドレスなど余計な文字列が末尾に書かれていることすら

だったりしますので、正規表現で取り除いてしまいましょう。

```ruby
# 建物名を正規化する
def norm_addr2(addr2)
	addr2_temp = addr2.clone
	# 括弧等は排除し、「○F」は「○階」と置き換える
	addr2_temp = addr2_temp.gsub(/\(.*/,'').gsub(/(\d+)F/, '\1階')
	# 「○階」「○号室」を含む場合、そこまでしか読み取らない
	addr2_temp = addr2_temp.match(/^.*号室/)[0] if addr2_temp.include?('号室')
	addr2_temp = addr2_temp.match(/^.*階/)[0] if addr2_temp.include?('階')
	# 別の住所を含んでいる場合、その部分だけ削除する
	pref_list().each{|pref|
		if addr2_temp.include?(pref)
			addr2_temp = addr2_temp[0, addr2_temp.index(pref)]
			break
		end
	}
	# 先頭・末尾の空白を削除する
	addr2_temp = addr2_temp.sub(/^ +/, '').sub(/ +$/, '')
	return addr2_temp
end
```