docker-composeを利用した逆ジオコーディングAPIを作った際の記録

サクッと使うにはどこぞの外部サービスを利用するのが早いですが，訳あって自製することになりました．

利用できるデータだとか，dockerを使った環境構築とかを書き留めておきたいと思います．


# システムの構成

http://server/rgeocode?lat=43.063751&lon=141.358524
のような形で緯度経度をパラメータとして渡すGETリクエストにより，住所の情報が入ったjsonがレスポンスボディとして返ってくるイメージです．
WebフロントエンドとしてAPIの機能を提供するためにFlaskを利用します．
住所データベースについてはPostgres(+PostGIS）を利用します．

# docker-composeを利用してサーバの環境構築を行う
docker-composeを利用してサーバ環境を構築します．
docker-compose.ymlとAPIサーバをFlaskを利用して提供するためのpython3環境を構築するためDockerfileを用意しましたので下記に示します．
※postgres-postgisコンテナのDBアクセスユーザーやパスワードについては外部公開する場合などには適宜修正してください

```yml:docker-compose.yml
version: '3'

services:
  postgres-postgis:
    container_name: postgres-postgis
    image: mdillon/postgis
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - ./docker/db/data:/var/lib/postgresql/data
      - ./docker/db/sql:/docker-entrypoint-initdb.d
      - ./opt:/root/opt
    ports:
    - 5435:5432
    
  python3env:
    restart: always
    build: python3
    container_name: 'python3env'
    working_dir: '/root/'
    tty: true
    volumes:
      - ./opt:/root/opt
    ports:
      - 56743:56743
```

```Dockerfile:python3/Dockerfile
FROM python:3
USER root

RUN apt-get update
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN apt-get install -y vim less
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
RUN pip install Flask
RUN pip install psycopg2
RUN pip install psycopg2-binary
```

## コンテナの起動
下記のコマンドで行います．一度バックグラウンド起動させずに実行して，エラーが出なかったら一旦止めて再度バックグラウンド起動するのがスムーズ．

```(console)
$ docker-compose up
(Ctrl+C)
$ docker-compose up -d
```

# 住所DBに利用する位置参照情報データの入手

住所DBについては国土交通省の位置参照情報データベースを利用して構築を行います．
（こういうすごいデータをタダで使えるんだから公共事業ってすごいよね）

https://nlftp.mlit.go.jp/isj/
にアクセスし左ペインの「位置参照情報 ダウンロード」を選択すると都道府県の選択画面が出ます．
今回は全国の街区単位のデータを集めるのでその手順について下記に示します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310137/524a4347-a322-3689-be06-76f5a1d526a2.png)

- 「都道府県単位」を選択
-  遷移先の画面で「市区町村全域」にチェックを入れる
- 「全て」のラジオボタンを選択する
- データ整備年度は「平成29年」のものを選択した上で「選択」ボタンを桜花する
- ※平成30年も選択できるようだが2020年7月1日現在ではデータがない模様


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310137/b5cb247a-8379-b6ad-c2e0-ee30bbb8a2aa.png)


- 「全ての街区レベルを選択」にチェックを入れて最下部までスクロールする
- 最下部に「選択」ボタンが有るので押下する
- 利用規約が表示されるのでよく読む

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310137/e3a7532a-5133-dbe8-7c44-c30edd002bde.png)

ファイルのダウンロード画面に遷移するので，47ファイルに対してを根気よく「ダウンロード」ボタンを押下してダウンロードする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310137/c9f0ca5f-dbf1-2642-20cd-ba924e3e66e6.png)


# 住所DBの構築

そろそろ書くのが面倒になってきたのでコマンドと初期化コード載せます
postgresとpythonのコンテナを./opt（コンテナ内では/root/opt）に永続化してありますので，ここに先程ダウンロードした位置参照情報のzipファイル群を入れます．

```(console)
# cd ./opt
$ chmod +x expandzip2csv.sh
$ ./download.sh
$ ./expandzip2csv.sh
$ docker-compose up -d
$ docker-compose exec postgres-postgis bash
$ mv ./opt/*-* /tmp/
$ cp ./opt/sample.sql /tmp
$ su postgres
$ psql -U postgres
$ postgres=> create database gis;
$ psql gis -U postgres
$ \i /tmp/sample.sql
```

位置参照情報のzipファイルを回答します．

```shell:expandzip2csv.sh
# 解凍
find ./ -name "*-16.0a.zip" -type f -print0 | xargs -0 -I {} unzip {} 

# 対象のcsvの改行コードを一括変換
find . -name '*.csv' -type f | xargs -n 10 nkf -Lu --overwrite

# 対象のCSV一覧を出力
find ./ -name "*.csv" -type f -print0 | xargs -0 -I {}  echo `pwd`/{} > list.txt
```

データベースを構築していきます．
※postgresのコンテナに入ってpsql実行する．

```(console)
$ psql -U postgres
$ postgres=>CREATE DATABASE gis;
$ postgres=>\q
$ psql gis -U postgres
```

```sql:sample.sql
-- 拡張をインストール
CREATE EXTENSION IF NOT EXISTS postgis;

-- テーブル削除
DROP TABLE IF EXISTS datalocation;

create table datalocation(
   prefectures_nm       varchar(100),   -- 都道府県名
   city_nm              varchar(100),   -- 市区町村名
   oaza_nm              varchar(100),   -- 大字・丁目名
   koaza_nm             varchar(100),   -- 小字・通称名
   block_no             varchar(100),   -- 街区符号・地番
   coordinate_no        varchar(100),    -- 座標系番号
   x_coordinate         numeric(8, 1),  -- Ｘ座標
   y_coordinate         numeric(8,1 ),  -- Ｙ座標
   latitude           varchar(15),      -- 緯度
   longitude          varchar(15),      -- 経度
   displayed_address_flg         char(1),   --住居表示フラグ
   representative_flg    char(1),   -- 代表フラグ
   before_history_flg    char(1),   -- 更新前履歴フラグ
   after_history_flg char(1)    -- 更新後履歴フラグ
);

SELECT AddGeometryColumn('datalocation', 'the_geom', 4612, 'POINT', 2);

update datalocationSET the_geom = ST_SETSRID(ST_POINT(longitude::float, latitude::float), 4612);

CREATE INDEX gist_loc_ref_info ON datalocation USING GIST (the_geom);
```

行数の確認．1900万行くらいありました．

```sql
SELECT count(*) from datalocation;
```


下記は東京タワーの緯度経度ですが，下記のSQL実行して東京タワーの住所がでてきたら成功です．

```sql
SELECT *
FROM   datalocation
ORDER BY ST_SETSRID(ST_POINT(139.745433, 35.658581), 4326) <-> the_geom
LIMIT 1;
```

# APIの構築を行う
ユーザーが逆ジオコーディングを行うために利用するAPIを構築します．
今回はpythonで動作するWebフロントエンドであるFlaskを利用します．

ソースベタ貼りします．
python3のコンテナ側からpostgresのコンテナに接続する際には，hostにコンテナ名を記載します．

```python:api.py
from flask import Flask, request
import psycopg2
import json

app = Flask(__name__)

# SELECT *
# FROM   m_address_gaiku
# WHERE  representative_flg = 1
# ORDER BY ST_SETSRID(ST_POINT(139.745433, 35.658581), 4326) <-> the_geom
# LIMIT 1;
def getAddress(lon, lat):
    users = 'postgres' #initial user
    dbnames = 'gis'
    passwords = 'postgres'
    host = 'postgres-postgis' 
    port = '5432'
    conn = psycopg2.connect(" user=" + users +" dbname=" + dbnames +" password=" + passwords +" host=" + host +" port=" + port)
    cur = conn.cursor()
    cur.execute("SELECT * FROM m_address_gaiku WHERE  representative_flg = '1' ORDER BY ST_SETSRID(ST_POINT("+str(lon)+", "+str(lat)+"), 4612) <-> the_geom LIMIT 1;")
    results = cur.fetchall()
    #cur.fetchone()
    #conn.commit()
    cur.close()
    conn.close()

    return results

def generate_response(result):
    ret = {
        'status':200,
        'result':
        {
            'prefecture': result[0][0],
            'municipality': result[0][1],
            'local' : 
            {
                'section':result[0][2],
                'homenumber':result[0][4],
                'longitude':result[0][9],
                'latitude':result[0][8]
            }
        },
        'meta':[
            {
                'name':'thanks',
                'content':'このサービスは 国土交通省 提供 街区レベル位置参照情報および大字・町丁目レベル位置参照情報 を利用しています'
            }
        ]
    }
    return ret;
    
def generate_error_response(code):
    ret = {
        'status':code,
        'result':
        {
            'prefecture': '',
            'municipality': '',
            'local' : 
            {
                'section':'',
                'homenumber':'',
                'longitude':'',
                'latitude':''
            }
        },
        'meta':[
            {
                'name':'thanks',
                'content':'このサービスは 国土交通省 提供 街区レベル位置参照情報および大字・町丁目レベル位置参照情報 を利用しています'
            }
        ]
    }
    return ret;

@app.route('/')
def root():
    ret = {}
    try:
        result = getAddress(139.745433, 35.658581)
        ret = generate_response(result)
    except:
        ret = generate_error_response(201)

    return json.dumps(ret)

# http://server/rgeocode?lat=43.063751&lon=141.358524
@app.route('/rgeocode', methods=['GET'])
def rgeocode():
    lat = request.args.get('lat', '')
    lon = request.args.get('lon', '')
    
    ret = {}
    try:
        result = getAddress(lon, lat)
        ret = generate_response(result)
    except:
        ret = generate_error_response(201)

    return json.dumps(ret)

@app.errorhandler(400)
@app.errorhandler(404)
@app.errorhandler(500)
def error_handler(error):
    response = {
        "status":error.code, 
        "error": 
        {
            "type": error.name, 
            "message": error.description 
        }
    }
    return json.dumps(response)

## おまじない
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=56743)
```

# 参考
ソース一式はそのうちまとめてこちらにおいておきます
https://github.com/nc30mtd/docker-reverse-geocoding-pgsql