PostGISで逆ジオコーディングしてみた

某勉強会で発表した内容を再構成したものです。

# きっかけ
今から18年前の平成17年。
我が国で初めてのスマートフォン、[W-ZERO3](https://jp.sharp/ws/004sh/index.html)が発売されたものの、発売当初はそれで利用出来るコンテンツはほぼ皆無と言える状況であったため、何が出来るのかを考えてみたら。

- PHS網を介して
	- Internetに簡単に接続出来る
	- **位置情報を取得出来る**
- 高い性能を持ち
    - MP3が余裕でデコード出来る
    - S-VHS程度の品質を持つ標準画質の動画がデコード出来る
- ユーザが任意でソフトウェアをインストールすることが出来る
	- OSはWindows CEであるため、そのソフトウェアはWindows用の開発環境で開発出来るみたい

今回は「位置情報を取得出来る」点に着目して、位置情報を利用したサービスの開発の検討をしてみることにした。

# 位置から住所を求めるには
![カーナビの画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518249/4215c248-c2a9-74fe-f6c3-8b9709907957.jpeg)
カーナビゲーションシステムで自車の位置を示す赤い矢印の場所の住所(神奈川県茅ヶ崎市)がその下部に表示されているが、このように位置から住所を求めることを一体何というのか。
調べてみるとこれは「**逆ジオコーディング**」と呼ばれているらしい。
これは一般的なパソコンやサーバで出来るのかも調べてみると。外部サービスを使うか、地図を扱えるソフトウェアをパソコンやサーバに導入することで出来るらしい。

# 逆ジオコーディングを外部サービスに任せる場合
- API呼び出し回数の制約
    - ユーザが3分に1回、データを間欠送信すると仮定して
        - 5万リクエスト/日 -> 110人まで ([Yahoo!リバースジオコーダーAPI](https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/reversegeocoder.html))
            - その上、商用利用は不可能
            - 仮にサービスが商用利用またはそれに準ずると位置づけられる場合は利用出来ない可能性あり
        - 10万リクエスト/日 -> 210人まで ([農研機構 簡易逆ジオコーディングサービス](https://www.naro.go.jp/collab/program/patent_category/patent_cate140/006902.html))
        - 1リクエスト/秒 -> 200人まで ([OpenStreetMap Nominatim API](https://nominatim.org/release-docs/latest/api/Reverse/))
    - いずれも1000人規模のサービスに対応出来るものではない
        - 有償のサービスではそれに耐えうる可能性の高いものも存在するようではあるが、そもそも利用者が存在するか分からないサービスに有償のものを使いたくはない
- ネットワーク越しに他のマシンにアクセスするため、その処理がオーバーヘッドとなる可能性が高い
- 精度は市区町村レベルで十分。字、街区、番地レベルの情報は必要無い

# 自分のマシンで出来ないのか
PostGISを導入したPostgreSQLに地図データを読み込ませれば可能。
## 使えそうな地図データを探す
- 国土交通省が公開している「[国土数値情報](https://nlftp.mlit.go.jp/ksj/)」内の「[行政区域](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-v3_1.html)」のデータが使える可能性が高い
    - [利用規約](https://nlftp.mlit.go.jp/ksj/other/agreement.html)を確認する限りでは出典を明記すれば商用利用も可能と思われる

# PostgreSQLに地図データを読み込ませる

```shell_session
PostgreSQLの管理ユーザに移行
# su – postgres

データベースを作成(ユーザにデータベース作成許可を与えている場合、管理ユーザでは無く常用しているユーザでも可)
$ createdb rev_geocoding

作成したデータベースでPostGISを有効にする(管理ユーザでないと有効には出来ないみたい)
rev_geocoding=# CREATE EXTENSION postgis;
rev_geocoding=# CREATE EXTENSION postgis_topology;

地図データをPostgreSQLに読める形に変換
$ shp2pgsql -s 4612 -D -i -I -W cp932 N03-23_230101.shp map > N03-23_230101.sql

先ほどPostGISを有効にしたデータベースに変換したデータを読み込み
$ psql -d rev_geocoding < N03-20230101_GML.sql
```

# PostgreSQLから逆ジオコーディングしてみる
## 投げるSQL文
```sql
SELECT n03_001,n03_002,n03_003,n03_004,n03_007 FROM map WHERE ST_Within( ST_GeometryFromText( ‘POINT([経度] [緯度])' , 4612 ) ,geom );
```
## 東経135.1961159度、北緯34.6903788度について要求を行った結果
```shell_session
$ psql -d rev_geocoding
psql (13.10)
"help"でヘルプを表示します。

rev_geocoding=> SELECT n03_001,n03_002,n03_003,n03_004,n03_007 FROM map WHERE ST_Within( ST_GeometryFromText( 'POINT(135.1961159 34.6903788)' , 4612 ) ,geom );
 n03_001 | n03_002 | n03_003 |   n03_004    | n03_007 
---------+---------+---------+--------------+---------
 兵庫県  |         | 神戸市  | 神戸市中央区 | 28110
(1 行)
```
# HTTPからアクセス出来るようにしてみる
SQL文を発行するためだけにRuby on Railsを使うのはさすがに大げさな話であると思うので、Javascript([Express.js](https://expressjs.com/ja/))で簡単に書いてみた。
JavascriptにはPostGIS専用のライブラリは存在しないため、[PostgreSQLのライブラリ](https://node-postgres.com/)で問題は無いらしい。
```javascript
"use strict";

const {Client,Pool} = require("pg");
const fs = require('fs');
const {parse} = require('csv-parse/sync');

const text = fs.readFileSync('./code_new.csv').toString();

const csv = parse(text, {
  columns: true,
});

const pool = new Pool({
    user: 'miyu',
    host: 'localhost',
    database: 'map',
    port: 5432
});
pool.connect();

module.exports.getLocation = function({longitude =0 ,latitude = 0}){
    // 位置情報から自治体コードを検索し
    var result = pool.query("SELECT n03_007 FROM map WHERE ST_Within( ST_GeometryFromText($1 , 4612 ) ,geom )",[`POINT(${longitude} ${latitude})`])
        .then(result => {
            // 検索結果最初の1件目の自治体コードが変換テーブル(code_new.csv)に含まれているかを検索
            return csv.find( ({ code }) => code == result.rows[0].n03_007);
        })
        .catch(error => {
            // 仮にエラーが発生した場合は0を返す。
            // 0には「位置情報なし」と登録されている。
            return csv.find( ({ code }) => code == 0);
        });
    return result;
};
```
## code_new.csvの内容は[全国地方公共団体コード](https://ja.wikipedia.org/wiki/%E5%85%A8%E5%9B%BD%E5%9C%B0%E6%96%B9%E5%85%AC%E5%85%B1%E5%9B%A3%E4%BD%93%E3%82%B3%E3%83%BC%E3%83%89)と対応する住所をCSVでまとめたもの
```
code,pref,city,town
0,位置情報なし,,
1101,北海道,札幌市,中央区
1102,北海道,札幌市,北区
1103,北海道,札幌市,東区
1104,北海道,札幌市,白石区
1105,北海道,札幌市,豊平区
1106,北海道,札幌市,南区
1107,北海道,札幌市,西区
1108,北海道,札幌市,厚別区
1109,北海道,札幌市,手稲区
1110,北海道,札幌市,清田区
1202,北海道,函館市,
1203,北海道,小樽市,
1204,北海道,旭川市,
1205,北海道,室蘭市,
1206,北海道,釧路市,
……
```
## HTTPでの待ち受け処理
```javascript
"use strict";

const port = 3000,
    express = require("express"),
    app = express();

const rev = require("./reverse_geocoding");

app.set("port",process.env.PORT || port);

app.use(
    express.urlencoded({
        extended: true
    })
);
app.use(express.json());

app.get("/getLocation",(request, response) =>{
    let longitude = request.query.longitude;
    let latitude = request.query.latitude;
    rev.getLocation({longitude: longitude,latitude: latitude})
        .then(result => {
            response.end(JSON.stringify(result));
        })
        .catch(error => {
            console.log("Error");
            response.end("Error");
        });
});
```
## HTTP経由で要求を出した際の返答
![スクリーンショット 2023-07-22 21.58.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518249/28b68404-7557-8995-18f9-fb6a7bc7a5d1.png)