【JavaScript】正規表現を駆使して住所部分の文字列だけを抜き出す

# はじめに
**郵便番号やビル情報が含まれる住所文字列**から**住所のみ**を抜き出したい！

例えば、下の表のようなことをしたかった訳です。

|元データ|抜き出したデータ|
|---|---|
|東京都新宿区新宿四丁目1番6号 (JR新宿ミライナタワー23階 カフェ) |東京都新宿区新宿四丁目1番6号|
|〒102-0082　東京都千代田区一番町8　住友不動産一番町ビル|東京都千代田区一番町8|

## なぜ？

1. 住所検索とかのAPIを使う際に、郵便番号やビル情報が不要だった（むしろ、あることで結果を正しく取得できなかったり）
2. Google先生でルートを検索しようとした時、そのままだと「ルート検索」が一発で出てこなかった

2に関しては、以下のような感じ。

* 「東京都 千代田区紀尾井町1-3 東京ガーデンテラス紀尾井町 ヤフー株式会社」 を検索した場合
<img width="500" alt="before.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/8715e920-562c-8246-6303-8889bfcd5b96.png">

* 「東京都千代田区紀尾井町1-3」 を検索した場合
<img width="500" alt="after.png" src="https://qiita-image-store.s3.amazonaws.com/0/33347/3993caba-e883-3acd-f371-fe2eadc2e97e.png">

# テストデータ準備
住所のデータは、connpassのAPIを使用して取得した情報を使います。
[**connpass API リファレンス**](https://connpass.com/about/api/)

御察しの方もいるかと思いますが、APIで取得した住所の表記がまちまちだったのが、そもそも住所部分だけを抜き出そうと思った最初のきっかけです。

# 実装
まずは最終成果物から。

```ts
export function getAddress( orgAddress: string ): string {
	let address = orgAddress;

	address = address.replace( /〒/g, "" );
	address = address.trim();
	address = address.replace( /^[0-9０-９]{3}-?[0-9０-９]{4}/g, "" );
	address = address.replace( /\(.*?\)/g, "" );
	address = address.replace( /（.*?）/g, "" );
	address = address.replace( /[0-9０-９]+[階|F].*?/g, "" );

	const allNumber: string = `([0-9０-９]+|[一二三四五六七八九十百千]+)`;
	const ary = address.match( new RegExp( `${allNumber}+(${allNumber}|(番町|丁目|丁|番地|番|号|-|‐|–|ー|−|－|の))*(${allNumber}|(丁目|丁|番地|番|号))`, "g" ) );
	if ( ary !== null ) {
		let addressLike = "";
		const len = ary.length;
		for ( let i = 0; i < len; i++ ) {
			const address = ary[ i ];
			if ( address.length >= addressLike.length ) {
				addressLike = address;
			}
		}
		const index = address.lastIndexOf( addressLike );
		address = address.substring( 0, index + addressLike.length );
	}

	address = address.replace( /\s/gi, "" );
	return address;
}
```

私がテストデータとして取得した100件ほどのデータはこれでいけました。
ただし、**全ての住所を正確に取得できるとは思わないでください。**

例えば、住所の区切りで使用する可能性のあるハイフンに似た文字は、以下の記事にある通りたくさんあります。
今回の実装でも、当初想定していたものと異なるハイフン系の文字が使われていたので、3つハイフン系の文字を加えました。

* [**ハイフンに似てる文字の文字コード - Qiita**](https://qiita.com/ryounagaoka/items/4cf5191d1a2763667add)

こんなコードでも役に立つなら自由に使っていただいて構わないのですが、各環境で動作確認をし、必要に応じてカスタマイズをしてくださいませ。

## 解説
以下、解説なので不要な方は読み飛ばしてください。

### 郵便番号を取り除く

```ts
	// 1
	address = address.replace( /〒/g, "" );

	// 2
	address = address.trim();

	// 3
	address = address.replace( /^[0-9０-９]{3}-?[0-9０-９]{4}/g, "" );
```

1. 「〒」マークがあったら空文字で置換（削除）
2. 先頭にスペースがあったら削除する  
（郵便番号の後にスペースが存在するパターンがあったので）
3. 前方一致で郵便番号っぽい数値は空文字で置換（削除）  
「数値3桁-数値4桁」あるいは「数値7桁」が対象  
半角全角の両方に対応するため、「\d」ではなく「[0-9０-９]」を指定

|入力データ|出力データ|
|---|---|
|東京都新宿区新宿四丁目1番6号 (JR新宿ミライナタワー23階 カフェ)|東京都新宿区新宿四丁目1番6号 (JR新宿ミライナタワー23階 カフェ)|
|〒102-0082　東京都千代田区一番町8　住友不動産一番町ビル|　東京都千代田区一番町8　住友不動産一番町ビル|

### 不要な文字列を取り除く

```ts
	// 1
	address = address.replace( /\(.*?\)/g, "" );

	// 2
	address = address.replace( /（.*?）/g, "" );

	// 3
	address = address.replace( /[0-9０-９]+[階|F].*?/g, "" );
```

1. 半角丸括弧と、その中身の文字列を空文字で置換（削除）
2. 全角丸括弧と、その中身の文字列を空文字で置換（削除）
3. 階層情報があった場合に、階層情報を空文字で置換（削除）  
この次の工程での誤検知を防ぐために削除しておく

|入力データ|出力データ|
|---|---|
|東京都新宿区新宿四丁目1番6号 (JR新宿ミライナタワー23階 カフェ)|東京都新宿区新宿四丁目1番6号|
|　東京都千代田区一番町8　住友不動産一番町ビル|　東京都千代田区一番町8　住友不動産一番町ビル|

### 建物情報を取り除く

```ts
	const allNumber: string = `([0-9０-９]+|[一二三四五六七八九十百千]+)`;
	const ary = address.match( new RegExp( `${allNumber}+(${allNumber}|(番町|丁目|丁|番地|番|号|-|‐|–|ー|−|－|の))*(${allNumber}|(丁目|丁|番地|番|号))`, "g" ) );
```

ここが一番ややこしいところなので分解していきます

① **`([0-9０-９]+|[一二三四五六七八九十百千]+)`**
「半角か全角か漢数字の数値が1つ以上」
漢数字も含めた数値を抜き出します。

|入力データ|取得リスト|
|---|---|
|東京都新宿区新宿四丁目1番6号|["四", "1", "6"]|
|　東京都千代田区一番町8　住友不動産一番町ビル|["千", "一", "8", "一"]|

② **`(${allNumber}|(番町|丁目|丁|番地|番|号|-|‐|–|ー|−|－|の))*`**
「半角か全角か漢数字の数値が1つ以上」もしくは「数字と数字をつなぐ文字」が0回以上

|入力データ|取得リスト|
|---|---|
|東京都新宿区新宿四丁目1番6号|["", "", "", "", "", "", "", "", "四丁目1番6号", ""]|
|　東京都千代田区一番町8　住友不動産一番町ビル|["", "", "", "", "千", "", "", "", "一番町8", "", "", "", "", "", "", "一番町", "", "", ""]|

③　**`(${allNumber}|(丁目|丁|番地|番|号))`**
「半角か全角か漢数字の数値が1つ以上」もしくは「丁目|丁|番地|番|号のいずれかの文字」
ようは住所の末尾が数字、もしくは指定文字列で終っていることを厳密に判断するためにつけてます。

|入力データ|取得リスト|
|---|---|
|東京都新宿区新宿四丁目1番6号|["四", "丁目", "1", "番", "6", "号"]|
|　東京都千代田区一番町8　住友不動産一番町ビル|["千", "一", "番", "8", "一", "番"]|

①+② **`${allNumber}+(${allNumber}|(番町|丁目|丁|番地|番|号|-|‐|–|ー|−|－|の))*`**

|入力データ|取得リスト|
|---|---|
|東京都新宿区新宿四丁目1番6号|["四丁目1番6号"]|
|　東京都千代田区一番町8　住友不動産一番町ビル|["千", "一番町8", "一番町"]|

①+②+③ **`${allNumber}+(${allNumber}|(番町|丁目|丁|番地|番|号|-|‐|–|ー|−|－|の))*(${allNumber}|(丁目|丁|番地|番|号))`**


|入力データ|取得リスト|
|---|---|
|東京都新宿区新宿四丁目1番6号|["四丁目1番6号"]|
|　東京都千代田区一番町8　住友不動産一番町ビル|["一番町8", "一番町"]|

### 番地情報より後方の文字列を取り除く

```ts
		let addressLike = "";
		const len = ary.length;
		for ( let i = 0; i < len; i++ ) {
			const address = ary[ i ];

			// 1
			if ( address.length >= addressLike.length ) {
				addressLike = address;
			}
		}

		// 2
		const index = address.lastIndexOf( addressLike );
		address = address.substring( 0, index + addressLike.length );
```

1. 条件にマッチした文字列のうち、一番長い文字列（長さが同じ場合は後勝ち）を取得する
2. 一番長い文字列が建物情報を除いた住所情報の末尾だと判断し、substringする

|入力データ|出力データ|
|---|---|
|東京都新宿区新宿四丁目1番6号|東京都新宿区新宿四丁目1番6号|
|　東京都千代田区一番町8　住友不動産一番町ビル|　東京都千代田区一番町8|

#### 「番町」を含んだ理由
もし仮に、前述の②に「番町」を含んでいなかった場合、どうなるか、
**`東京都千代田区一番町8　住友不動産一番町ビル`**
を例に考えてみます。

まず`番地情報より後方の文字列を取り除く`直前のリストが、`["一番", "8", "一番"]`となります。

すると、`条件にマッチした文字列のうち、一番長い文字列（長さが同じ場合は後勝ち）を取得する`工程において、`"一番"`が`addressLike`に格納されます。
その結果、`一番長い文字列が建物情報を除いた住所情報の末尾だと判断し、substringする`と、以下のようになってしまいます。

|入力データ|出力データ|
|---|---|
|　東京都千代田区**一番**町8　住友不動産**一番**町ビル|　東京都千代田区**一番**町8　住友不動産**一番**|

日本全国の住所を対象にすると、似た様に別途対応が必要な場所もあるのではないかと思います。

### 空白文字を取り除く

最後は、単純に空白文字を取り除きます。

```ts
address = address.replace( /\s/gi, "" );
```

|入力データ|出力データ|
|---|---|
|東京都新宿区新宿四丁目1番6号|東京都新宿区新宿四丁目1番6号|
|　東京都千代田区一番町8|東京都千代田区一番町8|

# 参考
* [**【Ruby】住所を検出・分割するAPIが欲しいので作ってみた - Qiita**](https://qiita.com/YSRKEN/items/5371e67950425ace6a73)  
※ こちらの記事を見つけた時点で8割から9割はやりたいことが完了したも同然でした!感謝です!