住所一覧から地図にピンを打つくん作ってみた

# 住所から緯度経度がほしいぞ
ひょんなひらめきからジオコーディング技術が必要になり、「**シンプル ジオコーディング 実験**」に参加したのでその記録を残します。

## インフルエンザの冬
乾燥の季節、冬です。
冬といえばインフルエンザが蔓延しますね。

みなさんおなじみの関東ITソフトウェア健康保険組合ではインフルエンザ予防接種の接種費用補助が受けられます。
https://www.its-kenpo.or.jp/kanri/influenza.html

これを利用しない手はないでしょう。

### 病院探しがちょっと手間
補助の対象となる病院は接種会場一覧のExcelファイルから確認できます。
しかし、すべての都道府県の情報が一つのファイルにまとまっていて、近所の病院だけを探し出すのが少し面倒です。
検索してしまっても良いですが、ここはクリエイティビティを発揮しましょう。

### 住所データから地図にピン打つくんを作る
理想を考えたとき、地図上に補助対象病院が記されている景色が脳裏をよぎりました。
そこで今回は、病院一覧データに含まれる住所情報を使って、病院の位置にピンを打ったマップを生成してみます。

## 実装について
https://github.com/RyumaRyama/address-csv-to-map

Webアプリケーションとして実装しました。
主な使用技術はこちら。

- Next.js
- Leafret
- シンプルジオコーディング

### Leafret
https://leafletjs.com/

地図表示ライブラリ。
強い意志があったわけではないですが、体感コスト低めに使えた気がします。
地図の画像データは別で用意する必要があり、そちらは [OpenStreetMap](https://www.openstreetmap.org/) を使用しました。

### シンプルジオコーディング
https://geocode.csis.u-tokyo.ac.jp/

#### シンプル ジオコーディング 実験って？
東京大学で行われている研究とのことです。
以下の内容を目的として行っているそうな。

> 本実験は，研究開発したジオコーディングエンジンを，特定のデータセットに含まれる住所表記ではなく，広く一般的に利用されている住所表記に対して適用し，その変換精度および変換速度を検証する目的で，試験として行っています．

[利用規約](https://geocode.csis.u-tokyo.ac.jp/home/simple-geocoding/simple-geocoding-tems-of-use/)に同意してAPIを使用することで実験参加となるようなので、参加してみました。

#### シンプルジオコーディング実験万歳
今回のジオコーディングではこちらの実験に助けられました。

地図上での位置マッピングにて難点となるのがジオコーディング。
住所から座標を求めるにはなかなかの課題があります。
有料サービスを使用することで解決する手もありますが、今回のようなお遊び実装では節約したい気持ちが出てしまいます。

この実験に出会うまでに自力で簡易ジオコーディングを頑張った際のつらい話は [おまけ](#おまけ) にて。

## 完成品がこちら
（リリースはサボったので動きが見たい方はリポジトリからどうぞ。）

地点の名称とその住所をCSV形式で与えると地図上にピンを打つ仕様です。
ITSの補助対象病院一覧は権利の問題で掲載できないので、気分で集めた愛知県にあるコメダ珈琲20件分のデータでお試し動作させます。
```csv
本店,愛知県名古屋市瑞穂区上山町３ー１４ー８
高岳店,愛知県名古屋市東区泉２ー２１ー３
松風店,愛知県名古屋市昭和区松風町１−７
上前津店,愛知県名古屋市中区大須３−３１−４２
東郊通店,愛知県名古屋市昭和区御器所１−３−１
五女子店,愛知県名古屋市中川区露橋２−２５−１５
刈谷店,愛知県刈谷市井ヶ谷町天白１９−４
本町店,愛知県名古屋市中区大須２−５−１３
新栄店,愛知県名古屋市中区新栄１ー１７ー１
平和店,愛知県名古屋市中区平和１−１０−１０コスモパレス
庄内通店,愛知県名古屋市西区庄内通４−１庄内ハイツ
浦里店,愛知県名古屋市緑区浦里１−１６０グランドハイツ浦里
児玉店,愛知県名古屋市西区児玉町３ー５ー１０
法華店,愛知県名古屋市中川区法華西町１−１３
野並店,愛知県名古屋市天白区井の森２３２−１アイコービル１Ｆ
中割店,愛知県名古屋市南区中割町３−１２０−１
山手店,愛知県名古屋市昭和区山手通４ー１５ペポ山手ビル２Ｆ
港店,愛知県名古屋市港区名港２−９−２７ポートプラザビル１Ｆ
城北店,愛知県名古屋市北区八代町２−１０−２
豊田梅坪店,愛知県豊田市梅坪町１−１８−１
```

このCSVファイルを読み込ませた結果がこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264913/1432e804-8896-8f50-0fc4-c9a7b3786eaf.png)

豊田市に住んでいて「近所の店舗だけ知りたいぞ」という場合は、絞り込みもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264913/a963f74d-4c17-da09-6cd8-ca70e1a197ba.png)

全件表示させてしまうとシンプルジオコーディング実験のサーバーに負荷をかけてしまうため、一度に最大20件までの実行としました。
そのため、本来の目的である病院位置のマッピング時には豊田市の実行例のように住んでいる地名などを入れることを想定しています。

## ジオコーディングAPIの利用

実装自体は変わったことをしているわけではないため、実装内容説明はAPI使用部について触れるのみにしておきます。

以下のように`addr`パラメータを指定することで緯度経度情報が含まれているXMLが返ってくる仕様でした。
`https://geocode.csis.u-tokyo.ac.jp/cgi-bin/simple_geocode.cgi?charset=UTF8&addr=愛知県名古屋市瑞穂区上山町３ー１４ー８`

実行結果
```xml
<results>
    <query>愛知県名古屋市瑞穂区上山町３ー１４ー８</query>
    <geodetic>wgs1984</geodetic>
    <iConf>5</iConf>
    <converted>愛知県名古屋市瑞穂区上山町３ー１４ー</converted>
    <candidate>
        <address>愛知県/名古屋市/瑞穂区/上山町/三丁目/１４番地</address>
        <longitude>136.949219</longitude>
        <latitude>35.137566</latitude>
        <iLvl>7</iLvl>
    </candidate>
</results>
```

読み込んだ住所一覧CSVファイルから取得した住所を使って上記APIを叩くことで緯度経度を得ています。
（jsミリしらなので参考程度にどうぞ）
```typescript
const updateMapData = () => {
    const reader = new FileReader();
    reader.onload = async (e) => {
      if (!e.target) return;
      const csv = e.target.result as string;
      const csv_data = parse<string[]>(csv, { header: false });

      const fetchMapData: MapData[] = [];

      // 件数を抑えるため、都道府県や検索ワードで絞り込みを行う
      const filtedData = csv_data.data.filter((result) => {
        return result[1].includes(pref) && result[1].includes(searchText);
      });

      Promise.all(filtedData.slice(0, maxPin).map(async (result) => {
        const name = result[0];
        const address = result[1];

        // パラメータに住所を追加してAPIを叩く
        const url = "https://geocode.csis.u-tokyo.ac.jp/cgi-bin/simple_geocode.cgi?charset=UTF8&addr=";
        await fetch(url + address)
          .then((res) => res.text())
          .then((data) => {
            const parser = new DOMParser();
            const doc = parser.parseFromString(data, "text/xml");
            const longitude = doc.getElementsByTagName("longitude")[0].textContent;
            const latitude = doc.getElementsByTagName("latitude")[0].textContent;
            fetchMapData.push({
              name: name,
              point: {
                longitude: parseFloat(longitude || "0"),
                latitude: parseFloat(latitude || "0"),
              },
            });
        });
      })).then(() => {
        setMapData(fetchMapData);
      });
    };
    if (!file) return;
    reader.readAsText(file);
  };
```

# おまけ
住所つらい話。
自力簡易ジオコーディングをしようとした結果、住所つらいなぁと感じたので印象的だった内容を残します。

## 市区町村名、大字・丁目名、小字・通称名 の分割が難しい
住所を記載する際、ほとんどの場合一つの文字列として扱ってしまいます。
これをジオコーディングなどで使用する場合はそれぞれの要素を分割する必要が出てくるわけです。
例えば今回使用したAPIの実行結果の中には住所を分割した結果が記されています。

`愛知県名古屋市瑞穂区上山町３ー１４ー８`
→ `愛知県/名古屋市/瑞穂区/上山町/三丁目/１４番地`

これが素人には無理すぎました。
名称の規則性には例外が多くあり、これらをひとまとまりに分割する仕組みを作るには住所に対する理解がまるで足りません。

世の中のジオコーディング装置どうやって実装されてるんだ...？

## 表記ゆれ
住所というものはとてもあいまいな表現が許されています。
例えば、
`ほげほげ町一丁目2-3`
`ほげほげ町1丁目2-3`
`ほげほげ町1-2-3`
`ほげほげ町１ー２ー３`
は同じ住所でしょう。

世の中の住所一覧はこれらの記載が混ざりに混ざっているため機械的に振り分けるのがとても難しいのです。

## 〇丁目
表記ゆれが解決されれば平和になると考えていた時代が私にもありました。

住所に頻出する「丁目」的な概念。
これは「`[数値] + 丁目`」というものではなく、文字列全体で名詞なのだとか。
なので一部地域では「〇〇丁」であったり、「〇〇丁目北」と表記されていたりします。

正しく表記されている場合はたいして問題にならないと思いますが、表記ゆれとの合わせ技でじわじわと苦しめてきます。

`ほげほげ町1-2-3`
`ほげほげ町1丁目北4-5`

のような混ざり方をされるとおしまいなのです。

# まとめ
なんとか理想のマップが生み出せたのでニコニコです。
ちょっとしたジオコーディングをしたい場合は今回参加した実験に参加してみると楽に実装できてうれしいかもしれません。
ついでに研究への貢献をしている感があってよいです。

住所がつらい問題については、人類が緯度経度を標準で使うようになれば解決ですね。（地盤は動いているので無理）