都道府県・都市・町情報をテーブル管理したい

# はじめに

都道府県・都市・町情報をサービスなどで管理するには、一般的にはGoogle Maps APIなどの力を借りてなるべく自分たちで実装しなくて済むようにするのが一番だと思います(住所などの情報は、頻繁ではないものの更新されていくものなので)

ただ、自前でテーブルを作ったりしないと検索機能をつくれなかったり、機械学習の特徴量として位置情報を利用したかったりなどの場合にはどうしても必要になるタイミングがあると思います。

# 政府統計の境界データ

[政府統計のe-Stat](https://www.e-stat.go.jp/gis/statmap-search?page=1&type=2&aggregateUnitForBoundary=A&toukeiCode=00200521&toukeiYear=2015&serveyId=A002005212015&datum=2000&coordsys=1&format=shape)というサイトがあり、ここでは各町毎の境界データがダウンロードできます。

今回は東京都全域の境界データをみていきます。

## shapeファイル

まずzipを解凍すると、見た事のない拡張子が並んでますが、これはすべて必要みたいです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/e7412089-36e1-9061-dc71-517515c2fbee.png" width="70%">

これらを総称してshapeファイルという(嘘だったらすみません)みたいですが、今回はこのファイルをPythonで扱いたいので必要なパッケージをインストールしていきます。

```sh
pip install pyshp shapely
```

もっと楽にデータの中身をみたい、という方はshapeファイルをpandasのデータフレームのように扱えるgeopandasというパッケージもインストールしておくと便利だと思います。

```sh
pip install geopandas
```

試しにgeopandasでshapeファイルを読み込んでみましょう。

```Python
import geopandas as gpd

df = gpd.read_file('h27ka13.shp')
```
こんな感じのデータになっていると思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/beede34c-a2b7-6940-a117-78e38478df34.png" width="90%">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/437488/eedd714d-731c-ed0c-abe0-66ebccd4b39b.png" width="90%">

ざっくりと必要なカラムについての説明をすると、

- PREF: 都道府県ID(2桁)
- CITY: 都市ID(3桁)
- S_AREA: 町ID(6桁)
- PREF_NAME: 都道府県名
- CITY_NAME: 都市名
- S_NAME: 町名
- geometry: 境界データ(Polygon)

です。(おそらく`S_NAME`の`S`はSTREETのSだと思われます)

# 地域情報テーブル

そのため、サービスとしてテーブルをつくるのであれば、

- `prefectures`テーブル
- `cities`テーブル
- `streets`テーブル

を用意すれば良いかと思われます。

あとは、ある地点の緯度経度から`prefecture_id`, `city_id`, `street_id`を導くことができればすべての住所がテーブル管理できます。

`prefecture_id`, `city_id`, `street_id`はそれぞれ桁数が`2`, `3`, `6`と決まっているのでこれらを文字列として連結したものを緯度経度から取得できればいいことになります。(これを`AddressCode`と呼ぶことにします)

## 緯度経度と各テーブルの紐付け

ある緯度経度がある町に含まれているということは、その町の境界データgeometryに含まれていることと同値なので、`AddressCode`を緯度経度から取得するスクリプトをつくりました。

```Python
import shapefile
from shapely.geometry import shape, Point


class AddressCodeGetter:
    def __init__(self, shp_path: str, encoding='cp932'):
        """shapeファイルを指定して読み込む."""
        r = shapefile.Reader(shp_path, encoding=encoding)
        self.shapes = r.shapes()
        self.records = r.records()

    def getAddressCode(self, latitude: float, longitude: float):
        """指定した緯度経度を含むPolygonを見つけて、そのAddressCodeを返す."""
        point = Point(longitude, latitude)
        AddressCode = ''
        for i in range(len(self.shapes)):
            polygon = shape(self.shapes[i])
            if polygon.contains(point):
                AddressCode = ''.join(self.records[i][1:4]) # PREF, CITY, S_AREAを文字列連結
                break
        return AddressCode


code = AddressCodeGetter('h27ka13.shp')
# 六本木ヒルズ森タワー(東京都港区六本木６丁目)
code.getAddressCode(35.660477, 139.729356) # '13103017006'
# 東京都庁(東京都新宿区西新宿２丁目)
code.getAddressCode(35.689604, 139.692305) # '13104094002'
# 皇居(東京都千代田区千代田)
code.getAddressCode(35.685756, 139.752292) # '13101014000'
# 渋谷スクランブルスクエア(東京都渋谷区渋谷２丁目)
code.getAddressCode(35.658778, 139.702487) # '13113010002'
```

これによって、経度経度からその地点の`prefecture_id`, `city_id`, `street_id`が求められることになりました。

例えば東京都庁の場合は、AddressCodeが`13104094002`なので

```json
{
  "prefecture_id": 13,
  "city_id": 104,
  "street_id": 94002
}
```

となります(street_idは`094002`なので数値化すると`94002`となる)

# おわりに

ある地点のAddressCodeを取得する際、緯度経度から取得するやり方ではなく住所から文字列操作によって行うやり方もありますが、これは結構大変です。意外と町名が最近変わっていたり、市町村合併されていたり、ユーザが入力した住所に旧字体が使われていたために認識できなかったり..など

特にユーザ入力させた住所をテーブルと紐付けるときには、Goole Maps APIから帰ってきた緯度経度を用いてAddressCodeを算出するなどして既存のAPIをうまく併用しながら開発すると快適なGeoライフが送れると思います！