matsuap commited on
Commit
a1c0952
·
verified ·
1 Parent(s): e5f0665

Upload 65 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .editorconfig +17 -0
  2. .gitignore +11 -0
  3. .tool-versions +1 -0
  4. DATA.md +12 -0
  5. Dockerfile +24 -0
  6. LICENSE +21 -0
  7. README.md +185 -10
  8. deploy/01_sync_to_s3.sh +14 -0
  9. deploy/01a_create_archive.sh +5 -0
  10. deploy/02_clear_cloudflare_cache.sh +11 -0
  11. docs/index.html +182 -0
  12. eslint.config.mjs +26 -0
  13. out/.keep +0 -0
  14. package-lock.json +0 -0
  15. package.json +63 -0
  16. src/01_make_prefecture_city.ts +12 -0
  17. src/02_make_machi_aza.ts +12 -0
  18. src/03_make_rsdt.ts +12 -0
  19. src/04_make_chiban.ts +12 -0
  20. src/10_refresh_csv_ranges.ts +10 -0
  21. src/99_create_stats.ts +9 -0
  22. src/address_data.proto +53 -0
  23. src/address_data.ts +763 -0
  24. src/data.ts +191 -0
  25. src/lib/abr_mlit_merge_tools.ts +29 -0
  26. src/lib/ckan.test.ts +29 -0
  27. src/lib/ckan.ts +167 -0
  28. src/lib/ckan_data/chiban.ts +83 -0
  29. src/lib/ckan_data/city.ts +70 -0
  30. src/lib/ckan_data/index.test.ts +55 -0
  31. src/lib/ckan_data/index.ts +70 -0
  32. src/lib/ckan_data/machi_aza.ts +117 -0
  33. src/lib/ckan_data/prefecture.ts +52 -0
  34. src/lib/ckan_data/rsdtdsp_rsdt.ts +88 -0
  35. src/lib/fetch_tools.ts +35 -0
  36. src/lib/mlit_nlftp.test.ts +20 -0
  37. src/lib/mlit_nlftp.ts +92 -0
  38. src/lib/proj.test.ts +12 -0
  39. src/lib/proj.ts +22 -0
  40. src/lib/settings.test.ts +59 -0
  41. src/lib/settings.ts +91 -0
  42. src/lib/zip_tools.test.ts +57 -0
  43. src/lib/zip_tools.ts +33 -0
  44. src/processes/01_make_prefecture_city.test.ts +32 -0
  45. src/processes/01_make_prefecture_city.ts +121 -0
  46. src/processes/02_machi_aza.ts +23 -0
  47. src/processes/02_make_machi_aza.test.ts +28 -0
  48. src/processes/02_make_machi_aza.ts +72 -0
  49. src/processes/03_make_rsdt.test.ts +26 -0
  50. src/processes/03_make_rsdt.ts +251 -0
.editorconfig ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EditorConfig is awesome: https://EditorConfig.org
2
+
3
+ # top-most EditorConfig file
4
+ root = true
5
+
6
+ # Unix-style newlines with a newline ending every file
7
+ [*]
8
+ end_of_line = lf
9
+ insert_final_newline = true
10
+ charset = utf-8
11
+ indent_style = space
12
+ indent_size = 2
13
+ trim_trailing_whitespace = true
14
+
15
+ [*.md]
16
+ indent_size = 4
17
+ indent_style = tab
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ dist
3
+ build
4
+ out
5
+ cache
6
+ .envrc
7
+ *.0x
8
+ *.7z
9
+ /settings.json
10
+ explorer
11
+ geolonia-japanese-addresses-v2-*.tgz
.tool-versions ADDED
@@ -0,0 +1 @@
 
 
1
+ nodejs 22.9.0
DATA.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # データ作成について
2
+
3
+ 下記元データのリンク
4
+
5
+ * [全アドレスデータ](https://catalog.registries.digital.go.jp/rc/dataset/ba000001) (位置参照拡張含む、**地番マスターは含まず**)
6
+ * [日本 都道府県マスター データセット `mt_pref_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-000000_g2-000001) [位置参照拡張 `mt_pref_pos_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-000000_g2-000012)
7
+ * [日本 市区町村マスター データセット `mt_city_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-000000_g2-000002) [位置参照拡張 `mt_city_pos_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-000000_g2-000013)
8
+ * [日本 町字マスター データセット `mt_town_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-000000_g2-000003) [位置参照拡張 `mt_town_pos_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba000004) <- こちらの mt_town_pos_all は zip 内に1つのCSVファイルではなく、それぞれの都道府県に分けられた csv.zip が含まれています
9
+ * [全国 住居表示-住居マスター データセット `mt_rsdtdsp_rsdt_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba000003) [位置参照拡張 `mt_rsdtdsp_rsdt_pos_all`](https://catalog.registries.digital.go.jp/rc/dataset/ba000006)
10
+ * 地番マスター(全国データなし、それぞれの市区町村個別でダウンロード)
11
+ * (都道府県)(市区町村) `mt_parcel_cityXXXXXX` 位置参照拡張 `mt_parcel_pos_cityXXXXXX`
12
+ * [北海道北斗市](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-012360_g2-000010) [位置参照拡張](https://catalog.registries.digital.go.jp/rc/dataset/ba-o1-012360_g2-000011)
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. ベースイメージにNode.jsを使う
2
+ FROM node:18
3
+
4
+ # 2. 作業ディレクトリを設定
5
+ WORKDIR /app
6
+
7
+ # 3. パッケージとソースをコピー
8
+ COPY package*.json ./
9
+ COPY . .
10
+
11
+ # 4. 依存関係をインストール
12
+ RUN npm install
13
+
14
+ # 5. ビルド(Viteを使用)
15
+ RUN npm run build
16
+
17
+ # 6. serve(静的ファイルサーバー)をグローバルにインストール
18
+ RUN npm install -g serve
19
+
20
+ # 7. ポート指定(HF Spacesは7860が標準)
21
+ EXPOSE 7860
22
+
23
+ # 8. buildフォルダを静的に提供
24
+ CMD ["serve", "-s", "dist", "-l", "7860"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Geolonia, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,185 @@
1
- ---
2
- title: Japanese Addresses V2
3
- emoji: 🐨
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Geolonia 住所データツール v2
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/%40geolonia%2Fjapanese-addresses-v2)](https://www.npmjs.com/package/@geolonia/japanese-addresses-v2)
4
+
5
+ 全国の住所データを HTTP API として公開するためのツールを公開いたします。
6
+
7
+ 本データは、デジタル庁が整備する「[アドレス・ベース・レジストリ](https://www.digital.go.jp/policies/base_registry_address)」を元に加工し、様々なアプリケーションから便利に使えるように整理したものとなります。
8
+
9
+ 以前、「[Geolonia 住所データ](https://github.com/geolonia/japanese-addresses)」を管理しましたが、v2は従来版と比べて下記の違いがあります。
10
+
11
+ * 住居表示住所データと対応(番地・号までのデータが含まれる)
12
+ * 地番住所のデータと対応(住居表示住所が導入されていない地域のデータが含まれる)
13
+
14
+ [リリースノート](https://github.com/geolonia/japanese-addresses-v2/releases)
15
+
16
+ ## API
17
+
18
+ このデータを使用した API をご提供しています。現在、制限無しの無料公開をしていますが、様子見ながら公開を停止や変更など行うことがあります。商用稼働は、ご自身でデータを作成しホスティングすることを強くおすすめします。 Geolonia は有償で管理・ホスティングするサービスありますので、ご利用の方はお問い合わせてください。
19
+
20
+ #### 都道府県エンドポイント
21
+
22
+ ```
23
+ https://japanese-addresses-v2.geoloniamaps.com/api/ja.json
24
+ ```
25
+
26
+ 例: [https://japanese-addresses-v2.geoloniamaps.com/api/ja.json](https://japanese-addresses-v2.geoloniamaps.com/api/ja.json)
27
+
28
+ ```
29
+ { "meta": { "updated": 00000 }, "data": [
30
+ {
31
+ "code": 10006,
32
+ "pref": "北海道",
33
+ "point": [
34
+ 141.347906782,
35
+ 43.0639406375
36
+ ]
37
+ "cities": [
38
+ {
39
+ "code": 11011,
40
+ "city": "札幌市",
41
+ "ward": "中央区",
42
+ "point": [
43
+ 141.35389,
44
+ 43.061414
45
+ ]
46
+ },
47
+ ...
48
+ ]
49
+ },
50
+ ...
51
+ ```
52
+
53
+ #### 町字エンドポイント
54
+
55
+ ```
56
+ https://japanese-addresses-v2.geoloniamaps.com/api/ja/<都道府県名>/<市区町村名>.json
57
+ ```
58
+
59
+ ※ 都道府県名及び市区町村名は URL エンコードを行ってください。
60
+
61
+ 例: [https://japanese-addresses-v2.geoloniamaps.com/api/ja/%E6%9D%B1%E4%BA%AC%E9%83%BD/%E6%96%B0%E5%AE%BF%E5%8C%BA.json](https://japanese-addresses-v2.geoloniamaps.com/api/ja/%E6%9D%B1%E4%BA%AC%E9%83%BD/%E6%96%B0%E5%AE%BF%E5%8C%BA.json)
62
+
63
+ ```
64
+ { "meta": { "updated": 00000 }, "data": [
65
+ ...
66
+ // 地番情報なしの住居表示未実施大字「新宿区北町」
67
+ {"machiaza_id":"0001000","oaza_cho":"北町","point":[139.735037,35.69995]},
68
+
69
+ // 地番情報ありの住居表示実施「新宿区新宿三丁目」
70
+ {"machiaza_id":"0020003","oaza_cho":"新宿","chome":"三丁目","rsdt":true,"point":[139.703563,35.691227],"csv_ranges":{"住居表示":{"start":151421,"length":19193},"地番":{"start":189779,"length":8895}}},
71
+ ...
72
+ ```
73
+
74
+ ### 注意
75
+
76
+ * 町丁目エンドポイントは、すべての地名を網羅しているわけではありません。
77
+ * アドレス・ベース・レジストリの整備状況によって住居表示が実施されている町字でも住居表示住所のデータが無いや、地番住所のデータが無いなどのことがあります。また、住居表示や地番の文字列が存在しても位置情報データがまだ存在しないケースもあります。
78
+
79
+ ## 住所データ・ API のビルド
80
+
81
+ ```shell
82
+ $ git clone [email protected]:geolonia/japanese-addresses-v2.git
83
+ $ cd japanese-addresses-v2
84
+ $ npm install
85
+ $ npm run run:all # APIを全て生成する
86
+
87
+ # オプション
88
+ $ npm run run:01_make_prefecture_city # 都道府県・市区町村のみ作成
89
+ $ npm run run:02_make_machi_aza # 町字API作成
90
+ $ npm run run:03_make_rsdt # 住居表示住所API作成 (町字APIが先に作らないとエラーになります)
91
+ $ npm run run:04_make_chiban # 地番住所API作成 (町字APIが先に作らないとエラーになります)
92
+ ```
93
+
94
+ #### APIビルド設定
95
+
96
+ `settings.json` に設定を入れてください。内容は `src/lib/settings.ts` を参照してください。
97
+
98
+ 例えば、出力を北海道にしぼりたい場合は下記のように設定してください。
99
+
100
+ ```json
101
+ {
102
+ "lgCodes": ["^01"]
103
+ }
104
+ ```
105
+
106
+ #### アーカイブファイル作成
107
+
108
+ `deploy/01a_create_archive.sh` を参照してください
109
+
110
+ ### API の構成
111
+
112
+ ```shell
113
+ └── api
114
+     ├── ja
115
+     │   │── {都道府県名}
116
+     │   │   ├── {市区町村名}-住居表示.txt
117
+     │   │   ├── {市区町村名}-地番.txt
118
+     │ │ └── {市区町村名}.json # 町字一覧
119
+     │ └── {都道府県名}.json # 市区町村一覧
120
+     └── ja.json # 都道府県と市区町村の一覧
121
+ ```
122
+
123
+ 各ファイルの詳細な仕様は、 `src/data.ts` の型定義を参照してください。
124
+
125
+ #### `txt` ファイル内のフォーマットについて
126
+
127
+ `-地番.txt` と `-住居表示.txt` は容量節約のため、市区町村の住所を全て一つのファイルに集約するものです。そのテキストファイルのフォーマットは下記となります。
128
+
129
+
130
+ ```
131
+ <町字1>,<start byte position>,<length in bytes>
132
+ <町字2>,<start byte position>,<length in bytes>
133
+ ...
134
+ =END=
135
+ <padding>地番,<町字1>
136
+ prc_num1,prc_num2,prc_num3,lng,lat
137
+ 1,,,<経度>,<緯度>
138
+ ...
139
+ ```
140
+
141
+ 論理的な構造としては
142
+
143
+ 1. ファイル全体のヘッダー
144
+ 1. ファイル内の町字(丁目、小字含む)一覧
145
+ 1. それぞれのデータのバイトレンジ(開始バイト数、容量バイト数)
146
+ 1. 町字データ(ループ)
147
+ 1. 町字データのヘッダー
148
+ 1. 地番・住居表示か
149
+ 1. カラム名定義
150
+ 1. 住所・位置情報データ
151
+
152
+ ヘッダーは5万バイトの倍数となります。末尾に `=END=` を挿入し、残りまで `0x20` (半角スペース)で埋めます。クライアントは `=END=` を確認できるまで、5万バイトずつ読み込むことをおすすめします。
153
+
154
+ また、 `api/ja/{都道府県名}/{市区町村名}.json` エンドポイントにも、 `csv_ranges` にヘッダーの start / length 情報入っているので、市区町村エンドポイントを既に読み込まれている場合はそのまま利用することをおすすめしております。
155
+
156
+ ## 出典
157
+
158
+ 本データは、以下のデータを元に、毎月 Geolonia にて更新作業を行っています。
159
+
160
+ 「アドレス・ベース・レジストリ」(デジタル庁)[住居表示住所・住居マスターデータセット](https://catalog.registries.digital.go.jp/) をもとに株式会社 Geolonia が作成したものです。
161
+
162
+ ## 貢献方法
163
+
164
+ * 本データに不具合がある場合には、[Issue](https://github.com/geolonia/japanese-addresses-v2/issues) または[プルリクエスト](https://github.com/geolonia/japanese-addresses-v2/pulls)にてご報告ください。
165
+
166
+ ## japanese-addresses-v2を使っているプロジェクト
167
+
168
+ * [normalize-japanese-addresses](https://github.com/geolonia/normalize-japanese-addresses) 日本の住所を正規化するライブラリ
169
+
170
+ ## スポンサー
171
+
172
+ * [一般社団法人 不動産テック協会](https://retechjapan.org/)
173
+
174
+ ## 関連情報
175
+
176
+ * [【プレスリリース】不動産テック協会、Geolonia と共同で不動産情報の共通 ID 付与の取り組みを開始](https://retechjapan.org/news/archives/pressrelease-20200731/)
177
+ * [【プレスリリース】日本全国の住所マスターデータをオープンデータとして無料公開](https://geolonia.com/pressrelease/2020/08/05/japanese-addresses.html)
178
+
179
+ ## ライセンス
180
+
181
+ Geolonia 住所データのライセンスは以下のとおりです。
182
+
183
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja)
184
+
185
+ 注: リポジトリに同梱されているデータ生成用のスクリプトのライセンスは MIT ライセンスとします。
deploy/01_sync_to_s3.sh ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash -e
2
+
3
+ aws s3 sync \
4
+ --cache-control="max-age=86400, public" \
5
+ --exclude="*" \
6
+ --include="*.txt" \
7
+ --content-type="text/plain; charset=utf-8" \
8
+ ./out/ s3://japanese-addresses-v2.geoloniamaps.com/
9
+ aws s3 sync \
10
+ --cache-control="max-age=86400, public" \
11
+ --exclude="*" \
12
+ --include="*.json" \
13
+ --content-type="application/json; charset=utf-8" \
14
+ ./out/ s3://japanese-addresses-v2.geoloniamaps.com/
deploy/01a_create_archive.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash -e
2
+
3
+ cd "$(dirname "$0")"/../out
4
+ tar -cf "api.tar" ./api
5
+ zstd -T0 -19 --rm -z "api.tar"
deploy/02_clear_cloudflare_cache.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash -e
2
+
3
+ curl --request POST \
4
+ --url https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache \
5
+ --header 'Content-Type: application/json' \
6
+ --header "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \
7
+ --data '{
8
+ "files": [
9
+ "https://japanese-addresses-v2.geoloniamaps.com/api/ja.json"
10
+ ]
11
+ }'
docs/index.html ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Geolonia japanese-addresses-v2</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
8
+ <script src="https://cdn.geolonia.com/v1/embed?geolonia-api-key=YOUR-API-KEY"></script>
9
+ <style type="text/css">
10
+ .hierarchical-list {
11
+ list-style-type: none;
12
+ padding-left: 20px;
13
+ position: relative;
14
+ }
15
+
16
+ .hierarchical-list li {
17
+ position: relative;
18
+ padding-left: 20px;
19
+ }
20
+
21
+ /* Vertical line connecting parent to child */
22
+ .hierarchical-list li::before {
23
+ content: "";
24
+ position: absolute;
25
+ top: 12px;
26
+ left: -10px;
27
+ border-left: 1px solid #777;
28
+ height: 100%;
29
+ width: 1px;
30
+ }
31
+
32
+ /* Horizontal line to connect list item to its parent */
33
+ .hierarchical-list li::after {
34
+ content: "";
35
+ position: absolute;
36
+ top: 12px;
37
+ left: -10px;
38
+ border-top: 1px solid #777;
39
+ width: 20px;
40
+ }
41
+ .hierarchical-list li:last-child::before {
42
+ content: "";
43
+ height: 12px;
44
+ }
45
+ .hierarchical-list ul {
46
+ padding-left: 20px;
47
+ list-style-type: none;
48
+ }
49
+ .hierarchical-list ul li::before {
50
+ top: 0;
51
+ height: 100%;
52
+ }
53
+ .hierarchical-list > li:last-child::before {
54
+ content: none;
55
+ }
56
+
57
+ .hierarchical-list li span {
58
+ margin-left: 10px;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div id="root"></div>
64
+
65
+ <div class="container" style="margin-top: 80px; border-top: 3px solid #dedede; padding: 8px;">
66
+ <p style="text-align: center;"><a href="https://github.com/geolonia/japanese-addresses-v2">@geolonia/japanese-addresses-v2</a></p>
67
+ </div>
68
+
69
+ <script src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
70
+ <script src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
71
+ <script type="module">
72
+ import htm from 'https://unpkg.com/[email protected]/dist/htm.module.js?module';
73
+ const html = htm.bind(React.createElement);
74
+ const API_ROOT = 'https://japanese-addresses-v2.geoloniamaps.com/api';
75
+
76
+ const toPercent = (num, total) => {
77
+ return ((num / total) * 100).toFixed(3);
78
+ };
79
+
80
+ const StatsTable = ({ stats }) => {
81
+ return html`
82
+ <ul className="hierarchical-list">
83
+ <li>
84
+ <strong>都道府県</strong>
85
+ <span>${stats.prefCount.toLocaleString()}</span>
86
+ </li>
87
+ <li>
88
+ <strong>市区町村 (政令指定都市の区を含む)</strong>
89
+ <span>${stats.lgCount.toLocaleString()}</span>
90
+ </li>
91
+ <li>
92
+ <strong>町字・丁目</strong>
93
+ <span>${stats.machiAzaCount.toLocaleString()}</span>
94
+ <ul>
95
+ <li>
96
+ <strong>住居表示あり</strong>
97
+ <span>${stats.machiAzaWithRsdtCount.toLocaleString()} (${toPercent(stats.machiAzaWithRsdtCount, stats.machiAzaCount)}%)</span>
98
+ </li>
99
+ <li>
100
+ <strong>地番あり</strong>
101
+ <span>${stats.machiAzaWithChibanCount.toLocaleString()} (${toPercent(stats.machiAzaWithChibanCount, stats.machiAzaCount)}%)</span>
102
+ </li>
103
+ <li>
104
+ <strong>住居表示と地番両方あり</strong>
105
+ <span>${stats.machiAzaWithChibanAndRsdtCount.toLocaleString()} (${toPercent(stats.machiAzaWithChibanAndRsdtCount, stats.machiAzaCount)}%)</span>
106
+ </li>
107
+ </ul>
108
+ </li>
109
+ <li>
110
+ <strong>住居表示住所</strong>
111
+ <span>${stats.rsdtCount.toLocaleString()}</span>
112
+ <ul>
113
+ <li>
114
+ <strong>位置情報あり</strong>
115
+ <span>${stats.rsdtWithPosCount.toLocaleString()} (${toPercent(stats.rsdtWithPosCount, stats.rsdtCount)}%)</span>
116
+ </li>
117
+ </ul>
118
+ </li>
119
+ <li>
120
+ <strong>地番住所</strong>
121
+ <span>${stats.chibanCount.toLocaleString()}</span>
122
+ <ul>
123
+ <li>
124
+ <strong>位置情報あり</strong>
125
+ <span>${stats.chibanWithPosCount.toLocaleString()} (${toPercent(stats.chibanWithPosCount, stats.chibanCount)}%)</span>
126
+ </li>
127
+ </ul>
128
+ </li>
129
+ </ul>
130
+ `;
131
+ };
132
+
133
+ const Stats = ({ updated }) => {
134
+ const [stats, setStats] = React.useState(null);
135
+
136
+ React.useEffect(() => {
137
+ fetch(`${API_ROOT}/stats.json?v=${updated}`)
138
+ .then((response) => response.json())
139
+ .then((data) => {
140
+ setStats(data);
141
+ });
142
+ }, [updated]);
143
+
144
+ const updatedFmt = new Date(updated * 1000).toLocaleString();
145
+
146
+ return html`
147
+ <div>
148
+ <h2>API内容の統計情報</h2>
149
+ <p>最終更新: ${updatedFmt}</p>
150
+ ${stats ? html`<${StatsTable} stats=${stats} />` : '読込中...'}
151
+ </div>
152
+ `;
153
+ };
154
+
155
+ const App = () => {
156
+ const [pref, setPref] = React.useState(null);
157
+ React.useEffect(() => {
158
+ fetch(`${API_ROOT}/ja.json`)
159
+ .then((response) => response.json())
160
+ .then((data) => {
161
+ setPref(data);
162
+ });
163
+ }, []);
164
+
165
+ return html`
166
+ <div class="container mt-4">
167
+ <h1><code>@geolonia/japanese-addresses-v2</code>について</h1>
168
+ <p>
169
+ 全国の都道府県、市区町村、町字・丁目、住居表示住所、地番住所のデータを HTTP API として提供しています。このページは、API の内容を確認するためのデモページです。
170
+ </p>
171
+ <p>
172
+ 詳しくは、 <a href="https://github.com/geolonia/japanese-addresses-v2">GitHub レポジトリ</a> をご覧ください。
173
+ </p>
174
+
175
+ ${pref ? html`<${Stats} updated=${pref.meta.updated} />` : null}
176
+ </div>
177
+ `;
178
+ }
179
+
180
+ ReactDOM.render(html`<${App} />`, document.getElementById("root"));
181
+ </script>
182
+ </body>
eslint.config.mjs ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //@ts-check
2
+ import eslint from '@eslint/js';
3
+ import tseslint from 'typescript-eslint';
4
+
5
+ export default tseslint.config(
6
+ eslint.configs.recommended,
7
+ tseslint.configs.recommended,
8
+ tseslint.configs.recommendedTypeChecked,
9
+ {
10
+ languageOptions: {
11
+ parserOptions: {
12
+ projectService: true,
13
+ tsconfigRootDir: import.meta.dirname,
14
+ },
15
+ }
16
+ },
17
+ {
18
+ ignores: [
19
+ 'cache',
20
+ 'node_modules',
21
+ 'dist',
22
+ 'eslint.config.mjs',
23
+ // '**/*.test.ts',
24
+ ],
25
+ }
26
+ );
out/.keep ADDED
File without changes
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@geolonia/japanese-addresses-v2",
3
+ "type": "module",
4
+ "version": "0.0.5",
5
+ "description": "",
6
+ "exports": {
7
+ "import": "./dist/esm/data.mjs",
8
+ "require": "./dist/cjs/data.cjs",
9
+ "types": "./dist/esm/data.d.ts"
10
+ },
11
+ "main": "./dist/cjs/data.cjs",
12
+ "types": "./dist/esm/data.d.ts",
13
+ "scripts": {
14
+ "prepack": "npm run clean && npm run build",
15
+ "clean": "shx rm -rf ./dist",
16
+ "build:proto": "protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. ./src/address_data.proto",
17
+ "build:dev": "tsc",
18
+ "build": "tsc -p tsconfig.dist-cjs.json && tsc -p tsconfig.dist-esm.json && mv ./dist/cjs/data.js ./dist/cjs/data.cjs && mv ./dist/esm/data.js ./dist/esm/data.mjs",
19
+ "clear:cache": "shx rm -rf ./cache",
20
+ "run:all": "npm run run:01_make_prefecture_city && npm run run:02_make_machi_aza && npm run run:03_make_rsdt && npm run run:04_make_chiban && npm run run:10_refresh_csv_ranges && npm run run:99_create_stats",
21
+ "run:01_make_prefecture_city": "tsx ./src/01_make_prefecture_city.ts",
22
+ "run:02_make_machi_aza": "tsx ./src/02_make_machi_aza.ts",
23
+ "run:03_make_rsdt": "node --max-old-space-size=8192 --import tsx ./src/03_make_rsdt.ts",
24
+ "run:04_make_chiban": "node --max-old-space-size=8192 --import tsx ./src/04_make_chiban.ts",
25
+ "run:10_refresh_csv_ranges": "tsx ./src/10_refresh_csv_ranges.ts",
26
+ "run:99_create_stats": "tsx ./src/99_create_stats.ts",
27
+ "create:archive": "rm ./api.7z; 7zz a ./api.7z ./out/api",
28
+ "start": "http-server --cors=\"*\" ./out",
29
+ "lint": "eslint ./src",
30
+ "test": "glob -c \"node --test --import tsx\" \"./src/**/*.test.ts\""
31
+ },
32
+ "keywords": [],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "files": [
36
+ "dist/**/*"
37
+ ],
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.17.0",
40
+ "@tsconfig/node22": "^22.0.0",
41
+ "@types/better-sqlite3": "^7.6.12",
42
+ "@types/cli-progress": "^3.11.6",
43
+ "@types/node": "^22.10.2",
44
+ "@types/proj4": "^2.5.5",
45
+ "@types/unzipper": "^0.10.10",
46
+ "better-sqlite3": "^11.7.2",
47
+ "cli-progress": "^3.12.0",
48
+ "csv-parse": "^5.6.0",
49
+ "eslint": "^9.17.0",
50
+ "glob": "^11.0.0",
51
+ "http-server": "^14.1.1",
52
+ "iconv-lite": "^0.6.3",
53
+ "lru-cache": "^11.0.2",
54
+ "proj4": "^2.12.1",
55
+ "shx": "^0.3.4",
56
+ "ts-proto": "^2.6.1",
57
+ "tsx": "^4.19.1",
58
+ "typescript": "^5.7.3",
59
+ "typescript-eslint": "^8.19.1",
60
+ "undici": "^7.2.1",
61
+ "unzipper": "^0.12.3"
62
+ }
63
+ }
src/01_make_prefecture_city.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import main from './processes/01_make_prefecture_city.js';
4
+
5
+ main(process.argv)
6
+ .then(() => {
7
+ process.exit(0);
8
+ })
9
+ .catch((e) => {
10
+ console.error(e);
11
+ process.exit(1);
12
+ });
src/02_make_machi_aza.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import main from './processes/02_make_machi_aza.js';
4
+
5
+ main(process.argv)
6
+ .then(() => {
7
+ process.exit(0);
8
+ })
9
+ .catch((e) => {
10
+ console.error(e);
11
+ process.exit(1);
12
+ });
src/03_make_rsdt.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import main from './processes/03_make_rsdt.js';
4
+
5
+ main(process.argv)
6
+ .then(() => {
7
+ process.exit(0);
8
+ })
9
+ .catch((e) => {
10
+ console.error(e);
11
+ process.exit(1);
12
+ });
src/04_make_chiban.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import main from './processes/04_make_chiban.js';
4
+
5
+ main(process.argv)
6
+ .then(() => {
7
+ process.exit(0);
8
+ })
9
+ .catch((e) => {
10
+ console.error(e);
11
+ process.exit(1);
12
+ });
src/10_refresh_csv_ranges.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import main from './processes/10_refresh_csv_ranges.js';
2
+
3
+ main(process.argv)
4
+ .then(() => {
5
+ process.exit(0);
6
+ })
7
+ .catch((e) => {
8
+ console.error(e);
9
+ process.exit(1);
10
+ });
src/99_create_stats.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import main from './processes/99_create_stats.js';
2
+
3
+ main(process.argv)
4
+ .then(() => {
5
+ process.exit(0);
6
+ }).catch((e) => {
7
+ console.error(e);
8
+ process.exit(1);
9
+ });
src/address_data.proto ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ message Header {
2
+ required Kind kind = 1;
3
+ // required string name = 2;
4
+ repeated HeaderRow rows = 3;
5
+ }
6
+
7
+ message HeaderRow {
8
+ required string name = 1;
9
+ required uint32 offset = 2;
10
+ required uint32 length = 3;
11
+ }
12
+
13
+ enum Kind {
14
+ RSDT = 0;
15
+ CHIBAN = 1;
16
+ }
17
+
18
+ message Section {
19
+ required Kind kind = 1;
20
+ required string name = 2;
21
+
22
+ repeated RsdtRow rsdt_rows = 3;
23
+ repeated ChibanRow chiban_rows = 4;
24
+ }
25
+
26
+ message RsdtRow {
27
+ optional uint32 blk_num = 1;
28
+ required uint32 rsdt_num = 2;
29
+ optional uint32 rsdt_num2 = 3;
30
+ optional LngLatPoint point = 4;
31
+
32
+ /** These strings are only used if the data cannot be expressed by an integer. */
33
+ optional string blk_num_str = 5;
34
+ optional string rsdt_num_str = 6;
35
+ optional string rsdt_num2_str = 7;
36
+ }
37
+
38
+ message ChibanRow {
39
+ required uint32 prc_num1 = 1;
40
+ optional uint32 prc_num2 = 2;
41
+ optional uint32 prc_num3 = 3;
42
+ optional LngLatPoint point = 4;
43
+
44
+ /** These strings are only used if the data cannot be expressed by an integer. */
45
+ optional string prc_num1_str = 5;
46
+ optional string prc_num2_str = 6;
47
+ optional string prc_num3_str = 7;
48
+ }
49
+
50
+ message LngLatPoint {
51
+ required double lng = 1;
52
+ required double lat = 2;
53
+ }
src/address_data.ts ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
2
+ // versions:
3
+ // protoc-gen-ts_proto v2.2.0
4
+ // protoc v5.28.1
5
+ // source: src/address_data.proto
6
+
7
+ /* eslint-disable */
8
+ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
9
+
10
+ export const protobufPackage = "";
11
+
12
+ export enum Kind {
13
+ RSDT = 0,
14
+ CHIBAN = 1,
15
+ UNRECOGNIZED = -1,
16
+ }
17
+
18
+ export function kindFromJSON(object: any): Kind {
19
+ switch (object) {
20
+ case 0:
21
+ case "RSDT":
22
+ return Kind.RSDT;
23
+ case 1:
24
+ case "CHIBAN":
25
+ return Kind.CHIBAN;
26
+ case -1:
27
+ case "UNRECOGNIZED":
28
+ default:
29
+ return Kind.UNRECOGNIZED;
30
+ }
31
+ }
32
+
33
+ export function kindToJSON(object: Kind): string {
34
+ switch (object) {
35
+ case Kind.RSDT:
36
+ return "RSDT";
37
+ case Kind.CHIBAN:
38
+ return "CHIBAN";
39
+ case Kind.UNRECOGNIZED:
40
+ default:
41
+ return "UNRECOGNIZED";
42
+ }
43
+ }
44
+
45
+ export interface Header {
46
+ kind: Kind;
47
+ /** required string name = 2; */
48
+ rows: HeaderRow[];
49
+ }
50
+
51
+ export interface HeaderRow {
52
+ name: string;
53
+ offset: number;
54
+ length: number;
55
+ }
56
+
57
+ export interface Section {
58
+ kind: Kind;
59
+ name: string;
60
+ rsdtRows: RsdtRow[];
61
+ chibanRows: ChibanRow[];
62
+ }
63
+
64
+ export interface RsdtRow {
65
+ blkNum?: number | undefined;
66
+ rsdtNum: number;
67
+ rsdtNum2?: number | undefined;
68
+ point?:
69
+ | LngLatPoint
70
+ | undefined;
71
+ /** These strings are only used if the data cannot be expressed by an integer. */
72
+ blkNumStr?: string | undefined;
73
+ rsdtNumStr?: string | undefined;
74
+ rsdtNum2Str?: string | undefined;
75
+ }
76
+
77
+ export interface ChibanRow {
78
+ prcNum1: number;
79
+ prcNum2?: number | undefined;
80
+ prcNum3?: number | undefined;
81
+ point?:
82
+ | LngLatPoint
83
+ | undefined;
84
+ /** These strings are only used if the data cannot be expressed by an integer. */
85
+ prcNum1Str?: string | undefined;
86
+ prcNum2Str?: string | undefined;
87
+ prcNum3Str?: string | undefined;
88
+ }
89
+
90
+ export interface LngLatPoint {
91
+ lng: number;
92
+ lat: number;
93
+ }
94
+
95
+ function createBaseHeader(): Header {
96
+ return { kind: 0, rows: [] };
97
+ }
98
+
99
+ export const Header: MessageFns<Header> = {
100
+ encode(message: Header, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
101
+ if (message.kind !== 0) {
102
+ writer.uint32(8).int32(message.kind);
103
+ }
104
+ for (const v of message.rows) {
105
+ HeaderRow.encode(v!, writer.uint32(26).fork()).join();
106
+ }
107
+ return writer;
108
+ },
109
+
110
+ decode(input: BinaryReader | Uint8Array, length?: number): Header {
111
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
112
+ let end = length === undefined ? reader.len : reader.pos + length;
113
+ const message = createBaseHeader();
114
+ while (reader.pos < end) {
115
+ const tag = reader.uint32();
116
+ switch (tag >>> 3) {
117
+ case 1:
118
+ if (tag !== 8) {
119
+ break;
120
+ }
121
+
122
+ message.kind = reader.int32() as any;
123
+ continue;
124
+ case 3:
125
+ if (tag !== 26) {
126
+ break;
127
+ }
128
+
129
+ message.rows.push(HeaderRow.decode(reader, reader.uint32()));
130
+ continue;
131
+ }
132
+ if ((tag & 7) === 4 || tag === 0) {
133
+ break;
134
+ }
135
+ reader.skip(tag & 7);
136
+ }
137
+ return message;
138
+ },
139
+
140
+ fromJSON(object: any): Header {
141
+ return {
142
+ kind: isSet(object.kind) ? kindFromJSON(object.kind) : 0,
143
+ rows: globalThis.Array.isArray(object?.rows) ? object.rows.map((e: any) => HeaderRow.fromJSON(e)) : [],
144
+ };
145
+ },
146
+
147
+ toJSON(message: Header): unknown {
148
+ const obj: any = {};
149
+ if (message.kind !== 0) {
150
+ obj.kind = kindToJSON(message.kind);
151
+ }
152
+ if (message.rows?.length) {
153
+ obj.rows = message.rows.map((e) => HeaderRow.toJSON(e));
154
+ }
155
+ return obj;
156
+ },
157
+
158
+ create<I extends Exact<DeepPartial<Header>, I>>(base?: I): Header {
159
+ return Header.fromPartial(base ?? ({} as any));
160
+ },
161
+ fromPartial<I extends Exact<DeepPartial<Header>, I>>(object: I): Header {
162
+ const message = createBaseHeader();
163
+ message.kind = object.kind ?? 0;
164
+ message.rows = object.rows?.map((e) => HeaderRow.fromPartial(e)) || [];
165
+ return message;
166
+ },
167
+ };
168
+
169
+ function createBaseHeaderRow(): HeaderRow {
170
+ return { name: "", offset: 0, length: 0 };
171
+ }
172
+
173
+ export const HeaderRow: MessageFns<HeaderRow> = {
174
+ encode(message: HeaderRow, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
175
+ if (message.name !== "") {
176
+ writer.uint32(10).string(message.name);
177
+ }
178
+ if (message.offset !== 0) {
179
+ writer.uint32(16).uint32(message.offset);
180
+ }
181
+ if (message.length !== 0) {
182
+ writer.uint32(24).uint32(message.length);
183
+ }
184
+ return writer;
185
+ },
186
+
187
+ decode(input: BinaryReader | Uint8Array, length?: number): HeaderRow {
188
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
189
+ let end = length === undefined ? reader.len : reader.pos + length;
190
+ const message = createBaseHeaderRow();
191
+ while (reader.pos < end) {
192
+ const tag = reader.uint32();
193
+ switch (tag >>> 3) {
194
+ case 1:
195
+ if (tag !== 10) {
196
+ break;
197
+ }
198
+
199
+ message.name = reader.string();
200
+ continue;
201
+ case 2:
202
+ if (tag !== 16) {
203
+ break;
204
+ }
205
+
206
+ message.offset = reader.uint32();
207
+ continue;
208
+ case 3:
209
+ if (tag !== 24) {
210
+ break;
211
+ }
212
+
213
+ message.length = reader.uint32();
214
+ continue;
215
+ }
216
+ if ((tag & 7) === 4 || tag === 0) {
217
+ break;
218
+ }
219
+ reader.skip(tag & 7);
220
+ }
221
+ return message;
222
+ },
223
+
224
+ fromJSON(object: any): HeaderRow {
225
+ return {
226
+ name: isSet(object.name) ? globalThis.String(object.name) : "",
227
+ offset: isSet(object.offset) ? globalThis.Number(object.offset) : 0,
228
+ length: isSet(object.length) ? globalThis.Number(object.length) : 0,
229
+ };
230
+ },
231
+
232
+ toJSON(message: HeaderRow): unknown {
233
+ const obj: any = {};
234
+ if (message.name !== "") {
235
+ obj.name = message.name;
236
+ }
237
+ if (message.offset !== 0) {
238
+ obj.offset = Math.round(message.offset);
239
+ }
240
+ if (message.length !== 0) {
241
+ obj.length = Math.round(message.length);
242
+ }
243
+ return obj;
244
+ },
245
+
246
+ create<I extends Exact<DeepPartial<HeaderRow>, I>>(base?: I): HeaderRow {
247
+ return HeaderRow.fromPartial(base ?? ({} as any));
248
+ },
249
+ fromPartial<I extends Exact<DeepPartial<HeaderRow>, I>>(object: I): HeaderRow {
250
+ const message = createBaseHeaderRow();
251
+ message.name = object.name ?? "";
252
+ message.offset = object.offset ?? 0;
253
+ message.length = object.length ?? 0;
254
+ return message;
255
+ },
256
+ };
257
+
258
+ function createBaseSection(): Section {
259
+ return { kind: 0, name: "", rsdtRows: [], chibanRows: [] };
260
+ }
261
+
262
+ export const Section: MessageFns<Section> = {
263
+ encode(message: Section, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
264
+ if (message.kind !== 0) {
265
+ writer.uint32(8).int32(message.kind);
266
+ }
267
+ if (message.name !== "") {
268
+ writer.uint32(18).string(message.name);
269
+ }
270
+ for (const v of message.rsdtRows) {
271
+ RsdtRow.encode(v!, writer.uint32(26).fork()).join();
272
+ }
273
+ for (const v of message.chibanRows) {
274
+ ChibanRow.encode(v!, writer.uint32(34).fork()).join();
275
+ }
276
+ return writer;
277
+ },
278
+
279
+ decode(input: BinaryReader | Uint8Array, length?: number): Section {
280
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
281
+ let end = length === undefined ? reader.len : reader.pos + length;
282
+ const message = createBaseSection();
283
+ while (reader.pos < end) {
284
+ const tag = reader.uint32();
285
+ switch (tag >>> 3) {
286
+ case 1:
287
+ if (tag !== 8) {
288
+ break;
289
+ }
290
+
291
+ message.kind = reader.int32() as any;
292
+ continue;
293
+ case 2:
294
+ if (tag !== 18) {
295
+ break;
296
+ }
297
+
298
+ message.name = reader.string();
299
+ continue;
300
+ case 3:
301
+ if (tag !== 26) {
302
+ break;
303
+ }
304
+
305
+ message.rsdtRows.push(RsdtRow.decode(reader, reader.uint32()));
306
+ continue;
307
+ case 4:
308
+ if (tag !== 34) {
309
+ break;
310
+ }
311
+
312
+ message.chibanRows.push(ChibanRow.decode(reader, reader.uint32()));
313
+ continue;
314
+ }
315
+ if ((tag & 7) === 4 || tag === 0) {
316
+ break;
317
+ }
318
+ reader.skip(tag & 7);
319
+ }
320
+ return message;
321
+ },
322
+
323
+ fromJSON(object: any): Section {
324
+ return {
325
+ kind: isSet(object.kind) ? kindFromJSON(object.kind) : 0,
326
+ name: isSet(object.name) ? globalThis.String(object.name) : "",
327
+ rsdtRows: globalThis.Array.isArray(object?.rsdtRows) ? object.rsdtRows.map((e: any) => RsdtRow.fromJSON(e)) : [],
328
+ chibanRows: globalThis.Array.isArray(object?.chibanRows)
329
+ ? object.chibanRows.map((e: any) => ChibanRow.fromJSON(e))
330
+ : [],
331
+ };
332
+ },
333
+
334
+ toJSON(message: Section): unknown {
335
+ const obj: any = {};
336
+ if (message.kind !== 0) {
337
+ obj.kind = kindToJSON(message.kind);
338
+ }
339
+ if (message.name !== "") {
340
+ obj.name = message.name;
341
+ }
342
+ if (message.rsdtRows?.length) {
343
+ obj.rsdtRows = message.rsdtRows.map((e) => RsdtRow.toJSON(e));
344
+ }
345
+ if (message.chibanRows?.length) {
346
+ obj.chibanRows = message.chibanRows.map((e) => ChibanRow.toJSON(e));
347
+ }
348
+ return obj;
349
+ },
350
+
351
+ create<I extends Exact<DeepPartial<Section>, I>>(base?: I): Section {
352
+ return Section.fromPartial(base ?? ({} as any));
353
+ },
354
+ fromPartial<I extends Exact<DeepPartial<Section>, I>>(object: I): Section {
355
+ const message = createBaseSection();
356
+ message.kind = object.kind ?? 0;
357
+ message.name = object.name ?? "";
358
+ message.rsdtRows = object.rsdtRows?.map((e) => RsdtRow.fromPartial(e)) || [];
359
+ message.chibanRows = object.chibanRows?.map((e) => ChibanRow.fromPartial(e)) || [];
360
+ return message;
361
+ },
362
+ };
363
+
364
+ function createBaseRsdtRow(): RsdtRow {
365
+ return { blkNum: 0, rsdtNum: 0, rsdtNum2: 0, point: undefined, blkNumStr: "", rsdtNumStr: "", rsdtNum2Str: "" };
366
+ }
367
+
368
+ export const RsdtRow: MessageFns<RsdtRow> = {
369
+ encode(message: RsdtRow, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
370
+ if (message.blkNum !== undefined && message.blkNum !== 0) {
371
+ writer.uint32(8).uint32(message.blkNum);
372
+ }
373
+ if (message.rsdtNum !== 0) {
374
+ writer.uint32(16).uint32(message.rsdtNum);
375
+ }
376
+ if (message.rsdtNum2 !== undefined && message.rsdtNum2 !== 0) {
377
+ writer.uint32(24).uint32(message.rsdtNum2);
378
+ }
379
+ if (message.point !== undefined) {
380
+ LngLatPoint.encode(message.point, writer.uint32(34).fork()).join();
381
+ }
382
+ if (message.blkNumStr !== undefined && message.blkNumStr !== "") {
383
+ writer.uint32(42).string(message.blkNumStr);
384
+ }
385
+ if (message.rsdtNumStr !== undefined && message.rsdtNumStr !== "") {
386
+ writer.uint32(50).string(message.rsdtNumStr);
387
+ }
388
+ if (message.rsdtNum2Str !== undefined && message.rsdtNum2Str !== "") {
389
+ writer.uint32(58).string(message.rsdtNum2Str);
390
+ }
391
+ return writer;
392
+ },
393
+
394
+ decode(input: BinaryReader | Uint8Array, length?: number): RsdtRow {
395
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
396
+ let end = length === undefined ? reader.len : reader.pos + length;
397
+ const message = createBaseRsdtRow();
398
+ while (reader.pos < end) {
399
+ const tag = reader.uint32();
400
+ switch (tag >>> 3) {
401
+ case 1:
402
+ if (tag !== 8) {
403
+ break;
404
+ }
405
+
406
+ message.blkNum = reader.uint32();
407
+ continue;
408
+ case 2:
409
+ if (tag !== 16) {
410
+ break;
411
+ }
412
+
413
+ message.rsdtNum = reader.uint32();
414
+ continue;
415
+ case 3:
416
+ if (tag !== 24) {
417
+ break;
418
+ }
419
+
420
+ message.rsdtNum2 = reader.uint32();
421
+ continue;
422
+ case 4:
423
+ if (tag !== 34) {
424
+ break;
425
+ }
426
+
427
+ message.point = LngLatPoint.decode(reader, reader.uint32());
428
+ continue;
429
+ case 5:
430
+ if (tag !== 42) {
431
+ break;
432
+ }
433
+
434
+ message.blkNumStr = reader.string();
435
+ continue;
436
+ case 6:
437
+ if (tag !== 50) {
438
+ break;
439
+ }
440
+
441
+ message.rsdtNumStr = reader.string();
442
+ continue;
443
+ case 7:
444
+ if (tag !== 58) {
445
+ break;
446
+ }
447
+
448
+ message.rsdtNum2Str = reader.string();
449
+ continue;
450
+ }
451
+ if ((tag & 7) === 4 || tag === 0) {
452
+ break;
453
+ }
454
+ reader.skip(tag & 7);
455
+ }
456
+ return message;
457
+ },
458
+
459
+ fromJSON(object: any): RsdtRow {
460
+ return {
461
+ blkNum: isSet(object.blkNum) ? globalThis.Number(object.blkNum) : 0,
462
+ rsdtNum: isSet(object.rsdtNum) ? globalThis.Number(object.rsdtNum) : 0,
463
+ rsdtNum2: isSet(object.rsdtNum2) ? globalThis.Number(object.rsdtNum2) : 0,
464
+ point: isSet(object.point) ? LngLatPoint.fromJSON(object.point) : undefined,
465
+ blkNumStr: isSet(object.blkNumStr) ? globalThis.String(object.blkNumStr) : "",
466
+ rsdtNumStr: isSet(object.rsdtNumStr) ? globalThis.String(object.rsdtNumStr) : "",
467
+ rsdtNum2Str: isSet(object.rsdtNum2Str) ? globalThis.String(object.rsdtNum2Str) : "",
468
+ };
469
+ },
470
+
471
+ toJSON(message: RsdtRow): unknown {
472
+ const obj: any = {};
473
+ if (message.blkNum !== undefined && message.blkNum !== 0) {
474
+ obj.blkNum = Math.round(message.blkNum);
475
+ }
476
+ if (message.rsdtNum !== 0) {
477
+ obj.rsdtNum = Math.round(message.rsdtNum);
478
+ }
479
+ if (message.rsdtNum2 !== undefined && message.rsdtNum2 !== 0) {
480
+ obj.rsdtNum2 = Math.round(message.rsdtNum2);
481
+ }
482
+ if (message.point !== undefined) {
483
+ obj.point = LngLatPoint.toJSON(message.point);
484
+ }
485
+ if (message.blkNumStr !== undefined && message.blkNumStr !== "") {
486
+ obj.blkNumStr = message.blkNumStr;
487
+ }
488
+ if (message.rsdtNumStr !== undefined && message.rsdtNumStr !== "") {
489
+ obj.rsdtNumStr = message.rsdtNumStr;
490
+ }
491
+ if (message.rsdtNum2Str !== undefined && message.rsdtNum2Str !== "") {
492
+ obj.rsdtNum2Str = message.rsdtNum2Str;
493
+ }
494
+ return obj;
495
+ },
496
+
497
+ create<I extends Exact<DeepPartial<RsdtRow>, I>>(base?: I): RsdtRow {
498
+ return RsdtRow.fromPartial(base ?? ({} as any));
499
+ },
500
+ fromPartial<I extends Exact<DeepPartial<RsdtRow>, I>>(object: I): RsdtRow {
501
+ const message = createBaseRsdtRow();
502
+ message.blkNum = object.blkNum ?? 0;
503
+ message.rsdtNum = object.rsdtNum ?? 0;
504
+ message.rsdtNum2 = object.rsdtNum2 ?? 0;
505
+ message.point = (object.point !== undefined && object.point !== null)
506
+ ? LngLatPoint.fromPartial(object.point)
507
+ : undefined;
508
+ message.blkNumStr = object.blkNumStr ?? "";
509
+ message.rsdtNumStr = object.rsdtNumStr ?? "";
510
+ message.rsdtNum2Str = object.rsdtNum2Str ?? "";
511
+ return message;
512
+ },
513
+ };
514
+
515
+ function createBaseChibanRow(): ChibanRow {
516
+ return { prcNum1: 0, prcNum2: 0, prcNum3: 0, point: undefined, prcNum1Str: "", prcNum2Str: "", prcNum3Str: "" };
517
+ }
518
+
519
+ export const ChibanRow: MessageFns<ChibanRow> = {
520
+ encode(message: ChibanRow, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
521
+ if (message.prcNum1 !== 0) {
522
+ writer.uint32(8).uint32(message.prcNum1);
523
+ }
524
+ if (message.prcNum2 !== undefined && message.prcNum2 !== 0) {
525
+ writer.uint32(16).uint32(message.prcNum2);
526
+ }
527
+ if (message.prcNum3 !== undefined && message.prcNum3 !== 0) {
528
+ writer.uint32(24).uint32(message.prcNum3);
529
+ }
530
+ if (message.point !== undefined) {
531
+ LngLatPoint.encode(message.point, writer.uint32(34).fork()).join();
532
+ }
533
+ if (message.prcNum1Str !== undefined && message.prcNum1Str !== "") {
534
+ writer.uint32(42).string(message.prcNum1Str);
535
+ }
536
+ if (message.prcNum2Str !== undefined && message.prcNum2Str !== "") {
537
+ writer.uint32(50).string(message.prcNum2Str);
538
+ }
539
+ if (message.prcNum3Str !== undefined && message.prcNum3Str !== "") {
540
+ writer.uint32(58).string(message.prcNum3Str);
541
+ }
542
+ return writer;
543
+ },
544
+
545
+ decode(input: BinaryReader | Uint8Array, length?: number): ChibanRow {
546
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
547
+ let end = length === undefined ? reader.len : reader.pos + length;
548
+ const message = createBaseChibanRow();
549
+ while (reader.pos < end) {
550
+ const tag = reader.uint32();
551
+ switch (tag >>> 3) {
552
+ case 1:
553
+ if (tag !== 8) {
554
+ break;
555
+ }
556
+
557
+ message.prcNum1 = reader.uint32();
558
+ continue;
559
+ case 2:
560
+ if (tag !== 16) {
561
+ break;
562
+ }
563
+
564
+ message.prcNum2 = reader.uint32();
565
+ continue;
566
+ case 3:
567
+ if (tag !== 24) {
568
+ break;
569
+ }
570
+
571
+ message.prcNum3 = reader.uint32();
572
+ continue;
573
+ case 4:
574
+ if (tag !== 34) {
575
+ break;
576
+ }
577
+
578
+ message.point = LngLatPoint.decode(reader, reader.uint32());
579
+ continue;
580
+ case 5:
581
+ if (tag !== 42) {
582
+ break;
583
+ }
584
+
585
+ message.prcNum1Str = reader.string();
586
+ continue;
587
+ case 6:
588
+ if (tag !== 50) {
589
+ break;
590
+ }
591
+
592
+ message.prcNum2Str = reader.string();
593
+ continue;
594
+ case 7:
595
+ if (tag !== 58) {
596
+ break;
597
+ }
598
+
599
+ message.prcNum3Str = reader.string();
600
+ continue;
601
+ }
602
+ if ((tag & 7) === 4 || tag === 0) {
603
+ break;
604
+ }
605
+ reader.skip(tag & 7);
606
+ }
607
+ return message;
608
+ },
609
+
610
+ fromJSON(object: any): ChibanRow {
611
+ return {
612
+ prcNum1: isSet(object.prcNum1) ? globalThis.Number(object.prcNum1) : 0,
613
+ prcNum2: isSet(object.prcNum2) ? globalThis.Number(object.prcNum2) : 0,
614
+ prcNum3: isSet(object.prcNum3) ? globalThis.Number(object.prcNum3) : 0,
615
+ point: isSet(object.point) ? LngLatPoint.fromJSON(object.point) : undefined,
616
+ prcNum1Str: isSet(object.prcNum1Str) ? globalThis.String(object.prcNum1Str) : "",
617
+ prcNum2Str: isSet(object.prcNum2Str) ? globalThis.String(object.prcNum2Str) : "",
618
+ prcNum3Str: isSet(object.prcNum3Str) ? globalThis.String(object.prcNum3Str) : "",
619
+ };
620
+ },
621
+
622
+ toJSON(message: ChibanRow): unknown {
623
+ const obj: any = {};
624
+ if (message.prcNum1 !== 0) {
625
+ obj.prcNum1 = Math.round(message.prcNum1);
626
+ }
627
+ if (message.prcNum2 !== undefined && message.prcNum2 !== 0) {
628
+ obj.prcNum2 = Math.round(message.prcNum2);
629
+ }
630
+ if (message.prcNum3 !== undefined && message.prcNum3 !== 0) {
631
+ obj.prcNum3 = Math.round(message.prcNum3);
632
+ }
633
+ if (message.point !== undefined) {
634
+ obj.point = LngLatPoint.toJSON(message.point);
635
+ }
636
+ if (message.prcNum1Str !== undefined && message.prcNum1Str !== "") {
637
+ obj.prcNum1Str = message.prcNum1Str;
638
+ }
639
+ if (message.prcNum2Str !== undefined && message.prcNum2Str !== "") {
640
+ obj.prcNum2Str = message.prcNum2Str;
641
+ }
642
+ if (message.prcNum3Str !== undefined && message.prcNum3Str !== "") {
643
+ obj.prcNum3Str = message.prcNum3Str;
644
+ }
645
+ return obj;
646
+ },
647
+
648
+ create<I extends Exact<DeepPartial<ChibanRow>, I>>(base?: I): ChibanRow {
649
+ return ChibanRow.fromPartial(base ?? ({} as any));
650
+ },
651
+ fromPartial<I extends Exact<DeepPartial<ChibanRow>, I>>(object: I): ChibanRow {
652
+ const message = createBaseChibanRow();
653
+ message.prcNum1 = object.prcNum1 ?? 0;
654
+ message.prcNum2 = object.prcNum2 ?? 0;
655
+ message.prcNum3 = object.prcNum3 ?? 0;
656
+ message.point = (object.point !== undefined && object.point !== null)
657
+ ? LngLatPoint.fromPartial(object.point)
658
+ : undefined;
659
+ message.prcNum1Str = object.prcNum1Str ?? "";
660
+ message.prcNum2Str = object.prcNum2Str ?? "";
661
+ message.prcNum3Str = object.prcNum3Str ?? "";
662
+ return message;
663
+ },
664
+ };
665
+
666
+ function createBaseLngLatPoint(): LngLatPoint {
667
+ return { lng: 0, lat: 0 };
668
+ }
669
+
670
+ export const LngLatPoint: MessageFns<LngLatPoint> = {
671
+ encode(message: LngLatPoint, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
672
+ if (message.lng !== 0) {
673
+ writer.uint32(9).double(message.lng);
674
+ }
675
+ if (message.lat !== 0) {
676
+ writer.uint32(17).double(message.lat);
677
+ }
678
+ return writer;
679
+ },
680
+
681
+ decode(input: BinaryReader | Uint8Array, length?: number): LngLatPoint {
682
+ const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
683
+ let end = length === undefined ? reader.len : reader.pos + length;
684
+ const message = createBaseLngLatPoint();
685
+ while (reader.pos < end) {
686
+ const tag = reader.uint32();
687
+ switch (tag >>> 3) {
688
+ case 1:
689
+ if (tag !== 9) {
690
+ break;
691
+ }
692
+
693
+ message.lng = reader.double();
694
+ continue;
695
+ case 2:
696
+ if (tag !== 17) {
697
+ break;
698
+ }
699
+
700
+ message.lat = reader.double();
701
+ continue;
702
+ }
703
+ if ((tag & 7) === 4 || tag === 0) {
704
+ break;
705
+ }
706
+ reader.skip(tag & 7);
707
+ }
708
+ return message;
709
+ },
710
+
711
+ fromJSON(object: any): LngLatPoint {
712
+ return {
713
+ lng: isSet(object.lng) ? globalThis.Number(object.lng) : 0,
714
+ lat: isSet(object.lat) ? globalThis.Number(object.lat) : 0,
715
+ };
716
+ },
717
+
718
+ toJSON(message: LngLatPoint): unknown {
719
+ const obj: any = {};
720
+ if (message.lng !== 0) {
721
+ obj.lng = message.lng;
722
+ }
723
+ if (message.lat !== 0) {
724
+ obj.lat = message.lat;
725
+ }
726
+ return obj;
727
+ },
728
+
729
+ create<I extends Exact<DeepPartial<LngLatPoint>, I>>(base?: I): LngLatPoint {
730
+ return LngLatPoint.fromPartial(base ?? ({} as any));
731
+ },
732
+ fromPartial<I extends Exact<DeepPartial<LngLatPoint>, I>>(object: I): LngLatPoint {
733
+ const message = createBaseLngLatPoint();
734
+ message.lng = object.lng ?? 0;
735
+ message.lat = object.lat ?? 0;
736
+ return message;
737
+ },
738
+ };
739
+
740
+ type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
741
+
742
+ export type DeepPartial<T> = T extends Builtin ? T
743
+ : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
744
+ : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
745
+ : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
746
+ : Partial<T>;
747
+
748
+ type KeysOfUnion<T> = T extends T ? keyof T : never;
749
+ export type Exact<P, I extends P> = P extends Builtin ? P
750
+ : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
751
+
752
+ function isSet(value: any): boolean {
753
+ return value !== null && value !== undefined;
754
+ }
755
+
756
+ export interface MessageFns<T> {
757
+ encode(message: T, writer?: BinaryWriter): BinaryWriter;
758
+ decode(input: BinaryReader | Uint8Array, length?: number): T;
759
+ fromJSON(object: any): T;
760
+ toJSON(message: T): unknown;
761
+ create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
762
+ fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
763
+ }
src/data.ts ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** 注意: [経度, 緯度] の順 */
2
+ export type LngLat = [number, number];
3
+
4
+ export type SinglePrefecture = {
5
+ /** 全国地方公共団体コード */
6
+ code: number;
7
+ /** 都道府県名 */
8
+ pref: string;
9
+
10
+ /** 都道府県名 (カナ) */
11
+ pref_k: string;
12
+
13
+ /** 都道府県名 (ローマ字) */
14
+ pref_r: string;
15
+
16
+ /** 代表点 (県庁の位置) */
17
+ point: LngLat;
18
+
19
+ cities: SingleCity[];
20
+ };
21
+
22
+ /**
23
+ * SinglePrefecture から都道府県名を取得します。
24
+ * @param pref SinglePrefecture
25
+ * @returns string
26
+ */
27
+ export function prefectureName(pref: SinglePrefecture): string {
28
+ return pref.pref;
29
+ }
30
+
31
+ type Api<T> = {
32
+ meta: {
33
+ /** データ更新日(UNIX時間; 秒) */
34
+ updated: number;
35
+ };
36
+ data: T;
37
+ }
38
+
39
+ /**
40
+ * 都道府県、市区町村一覧API
41
+ * 政令都市の場合は区で区切ります
42
+ * @file api/ja.json
43
+ */
44
+ export type PrefectureApi = Api<SinglePrefecture[]>;
45
+
46
+ export type SingleCity = {
47
+ /** 全国地方公共団体コード */
48
+ code: number;
49
+ /** 郡名 */
50
+ county?: string;
51
+ /** 郡名 (カナ) */
52
+ county_k?: string;
53
+ /** 郡名 (ローマ字) */
54
+ county_r?: string;
55
+
56
+ /** 市区町村名 */
57
+ city: string;
58
+ /** 市区町村名 (カナ) */
59
+ city_k: string;
60
+ /** 市区町村名 (ローマ字) */
61
+ city_r: string;
62
+
63
+ /** 政令市区名 */
64
+ ward?: string;
65
+ /** 政令市区名 (カナ) */
66
+ ward_k?: string;
67
+ /** 政令市区名 (ローマ字) */
68
+ ward_r?: string;
69
+
70
+ /** 代表点 (自治体役場の位置) */
71
+ point: LngLat;
72
+ };
73
+
74
+ /**
75
+ * SingleCity から市区町村名を取得します。郡名と政令市区名を含めます。
76
+ * @param city SingleCity
77
+ * @returns string
78
+ */
79
+ export function cityName(city: SingleCity): string {
80
+ return `${city.county || ''}${city.city}${city.ward || ''}`;
81
+ }
82
+
83
+ /**
84
+ * 市区町村一覧API
85
+ * @file api/ja/{都道府県名}.json
86
+ */
87
+ export type CityApi = Api<SingleCity[]>;
88
+
89
+ export type SingleMachiAza = {
90
+ /** ABR上の「町字ID」 */
91
+ machiaza_id: string;
92
+
93
+ /** 大字・町名 */
94
+ oaza_cho?: string;
95
+ /** 大字・町名 (カナ) */
96
+ oaza_cho_k?: string;
97
+ /** 大字・町名 (ローマ字) */
98
+ oaza_cho_r?: string;
99
+
100
+ /** 丁目名 */
101
+ chome?: string;
102
+ /** 丁目名 (数字) */
103
+ chome_n?: number;
104
+
105
+ /** 小字名 */
106
+ koaza?: string;
107
+ /** 小字名 (カナ) */
108
+ koaza_k?: string;
109
+ /** 小字名 (ローマ字) */
110
+ koaza_r?: string;
111
+
112
+ /** 住居表示住所の情報の存在。値が存在しない場合は、住居表示住所の情報は存在しません。 */
113
+ rsdt?: true;
114
+
115
+ /** 代表点 */
116
+ point?: LngLat;
117
+
118
+ /** CSV APIに付加情報が存在する場合、この町字のバイト範囲を指定します。 */
119
+ csv_ranges?: {
120
+ ["住居表示"]?: { start: number; length: number; };
121
+ ["地番"]?: { start: number; length: number; };
122
+ }
123
+ };
124
+
125
+ /**
126
+ * SingleMachiAza から町字名を取得します。大字・丁目・小字を含めます。
127
+ * @param machiAza SingleMachiAza
128
+ * @returns string
129
+ */
130
+ export function machiAzaName(machiAza: SingleMachiAza): string {
131
+ return `${machiAza.oaza_cho || ''}${machiAza.chome || ''}${machiAza.koaza || ''}`;
132
+ }
133
+
134
+ /**
135
+ * 町字一覧API
136
+ * @file api/ja/{都道府県名}/{市区町村名}.json
137
+ */
138
+ export type MachiAzaApi = Api<SingleMachiAza[]>;
139
+
140
+ export type SingleRsdt = {
141
+ /** 街区符号 */
142
+ blk_num?: string;
143
+ /** 住居番号 */
144
+ rsdt_num: string;
145
+ /** 住居番号2 */
146
+ rsdt_num2?: string;
147
+
148
+ /** 代表点 */
149
+ point?: LngLat;
150
+ };
151
+
152
+ /**
153
+ * SingleRsdt の街区符号・住居番号・住居番号2を `-` で区切った文字列を返します。
154
+ * @param rsdt SingleRsdt
155
+ * @returns string
156
+ */
157
+ export function rsdtToString(rsdt: SingleRsdt): string {
158
+ return [rsdt.blk_num, rsdt.rsdt_num, rsdt.rsdt_num2].filter(Boolean).join('-');
159
+ }
160
+
161
+ /**
162
+ * {市区町村名}-住居表示.json は類似なデータフォーマットを使います。
163
+ * @file api/ja/{都道府県名}/{市区町村名}-住居表示.json
164
+ */
165
+ export type RsdtApi = {
166
+ machiAza: SingleMachiAza;
167
+ rsdts: SingleRsdt[];
168
+ }[];
169
+
170
+ export type SingleChiban = {
171
+ /** 地番1 */
172
+ prc_num1: string;
173
+ /** 地番2 */
174
+ prc_num2?: string;
175
+ /** 地番3 */
176
+ prc_num3?: string;
177
+
178
+ /** 代表点 */
179
+ point?: LngLat;
180
+ };
181
+
182
+ /**
183
+ * SingleChiban の地番1・地番2・地番3を `-` で区切った文字列を返します。
184
+ * @param chiban SingleChiban
185
+ * @returns string
186
+ */
187
+ export function chibanToString(chiban: SingleChiban): string {
188
+ return [chiban.prc_num1, chiban.prc_num2, chiban.prc_num3]
189
+ .filter(Boolean)
190
+ .join('-');
191
+ }
src/lib/abr_mlit_merge_tools.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SingleMachiAza } from "../data.js";
2
+ import { NlftpMlitDataRow } from "./mlit_nlftp.js";
3
+
4
+ export function filterMlitDataByPrefCity(mlitData: NlftpMlitDataRow[], prefName: string, cityName: string): NlftpMlitDataRow[] {
5
+ return mlitData.filter(row => row.pref_name === prefName && row.city_name === cityName);
6
+ }
7
+
8
+ export function createMergedApiData(abrData: SingleMachiAza[], mlitData: NlftpMlitDataRow[]): SingleMachiAza[] {
9
+ const out = abrData;
10
+
11
+ for (const row of mlitData) {
12
+ // ABRデータに重複があるかのチェック
13
+ if (abrData.find(a => (
14
+ (a.oaza_cho === row.oaza_cho && a.chome === row.chome) || // 大字と丁目が一致する場合
15
+ (a.koaza === row.oaza_cho) || // 小字が一致する場合
16
+ ((a.oaza_cho || '') + (a.koaza || '') === row.oaza_cho) // 大字と小字を結合したものが一致する場合
17
+ ))) {
18
+ continue;
19
+ }
20
+ out.push({
21
+ machiaza_id: row.machiaza_id,
22
+ oaza_cho: row.oaza_cho,
23
+ chome: row.chome,
24
+ point: row.point,
25
+ })
26
+ }
27
+
28
+ return out;
29
+ }
src/lib/ckan.test.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import * as ckan from './ckan.js';
5
+
6
+ await test.describe('ckan', async () => {
7
+ await test('ckanPackageSearch works', async () => {
8
+ const res = await ckan.ckanPackageSearch('香川県高松市');
9
+ assert.ok(res.length > 0);
10
+ });
11
+
12
+ await test('getCkanPackageById works', async () => {
13
+ const res = await ckan.getCkanPackageById('ba-o1-000000_g2-000001');
14
+ assert.strictEqual(res.name, 'ba-o1-000000_g2-000001');
15
+ });
16
+
17
+ await test.describe('downloadAndExtract', async () => {
18
+ await test('should download, unzip, and parse the CSV file', async () => {
19
+ const res = ckan.downloadAndExtract<Record<string, string>>('https://catalog.registries.digital.go.jp/rsc/address/mt_town_city372013.csv.zip');
20
+ let count = 0;
21
+ for await (const row of res) {
22
+ count += 1;
23
+ // make sure all rows are parsed, and the header row is not in the results
24
+ assert.strictEqual(row['lg_code'], '372013');
25
+ }
26
+ assert.ok(count > 0);
27
+ });
28
+ });
29
+ });
src/lib/ckan.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ import { parse as csvParse } from 'csv-parse';
5
+
6
+ import { fetch } from 'undici';
7
+ import { unzipAndExtractZipBuffer } from './zip_tools.js';
8
+ import { getDownloadStream } from './fetch_tools.js';
9
+ import { lgCodeMatch, loadSettings } from './settings.js';
10
+
11
+ const CKAN_BASE_REGISTRY_URL = `https://catalog.registries.digital.go.jp/rc`
12
+ const USER_AGENT = 'curl/8.7.1';
13
+ const CACHE_DIR = path.join(import.meta.dirname, '..', '..', 'cache');
14
+
15
+ export type CKANResponse<T = CKANResponseInner> = {
16
+ success: false
17
+ } | {
18
+ success: true
19
+ result: T
20
+ }
21
+
22
+ type CKANResponseInner = (
23
+ CKANPackageSearchResultList |
24
+ CKANPackageSearchResult
25
+ );
26
+
27
+ export type CKANPackageSearchResultList = {
28
+ count: number,
29
+ sort: string,
30
+ results: CKANPackageSearchResult[]
31
+ }
32
+
33
+ export type CKANPackageSearchResult = {
34
+ id: string
35
+ metadata_created: string,
36
+ metadata_modified: string,
37
+ name: string,
38
+ notes: string,
39
+ num_resources: number,
40
+ num_tags: number,
41
+ title: string,
42
+ type: string,
43
+ extras: { [key: string]: string }[],
44
+ resources: CKANPackageResource[],
45
+ }
46
+
47
+ export type CKANPackageResource = {
48
+ description: string
49
+ last_modified: string
50
+ id: string
51
+ url: string
52
+ format: string
53
+ }
54
+
55
+ export async function ckanPackageSearch(query: string): Promise<CKANPackageSearchResult[]> {
56
+ const cacheKey = `package_search_${query}.json`;
57
+ const cacheFile = path.join(CACHE_DIR, 'ckan', cacheKey);
58
+
59
+ let json: CKANResponse<CKANPackageSearchResultList>;
60
+ if (fs.existsSync(cacheFile)) {
61
+ json = await fs.promises.readFile(cacheFile, 'utf-8')
62
+ .then((data) => JSON.parse(data) as CKANResponse<CKANPackageSearchResultList>);
63
+ } else {
64
+ const url = new URL(`${CKAN_BASE_REGISTRY_URL}/api/3/action/package_search`);
65
+ url.searchParams.set('q', query);
66
+ const res = await fetch(url.toString(), {
67
+ headers: {
68
+ 'User-Agent': USER_AGENT,
69
+ },
70
+ });
71
+ json = await res.json() as CKANResponse<CKANPackageSearchResultList>;
72
+
73
+ await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true });
74
+ await fs.promises.writeFile(cacheFile, JSON.stringify(json));
75
+ }
76
+
77
+ if (!json.success) {
78
+ throw new Error('CKAN API returned an error: ' + JSON.stringify(json));
79
+ }
80
+
81
+ return json.result.results;
82
+ }
83
+
84
+ export async function getCkanPackageById(id: string): Promise<CKANPackageSearchResult> {
85
+ const cacheKey = `package_show_${id}.json`;
86
+ const cacheFile = path.join(CACHE_DIR, 'ckan', cacheKey);
87
+
88
+ let json: CKANResponse<CKANPackageSearchResult>;
89
+ if (fs.existsSync(cacheFile)) {
90
+ json = await fs.promises.readFile(cacheFile, 'utf-8')
91
+ .then((data) => JSON.parse(data) as CKANResponse<CKANPackageSearchResult>);
92
+ } else {
93
+ const url = new URL(`${CKAN_BASE_REGISTRY_URL}/api/3/action/package_show`);
94
+ url.searchParams.set('id', id);
95
+ const res = await fetch(url.toString(), {
96
+ headers: {
97
+ 'User-Agent': USER_AGENT,
98
+ },
99
+ });
100
+ json = await res.json() as CKANResponse<CKANPackageSearchResult>;
101
+
102
+ await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true });
103
+ await fs.promises.writeFile(cacheFile, JSON.stringify(json));
104
+ }
105
+ if (!json.success) {
106
+ throw new Error('CKAN API returned an error: ' + JSON.stringify(json));
107
+ }
108
+ return json.result;
109
+ }
110
+
111
+ export function getUrlForCSVResource(res: CKANPackageSearchResult): string | undefined {
112
+ return res.resources.find((resource) => resource.format.startsWith('CSV'))?.url;
113
+ }
114
+
115
+ export type CSVParserIterator<T> = AsyncIterableIterator<T>;
116
+
117
+ export async function *combineCSVParserIterators<T>(...iterators: CSVParserIterator<T>[]): CSVParserIterator<T> {
118
+ for (const i of iterators) {
119
+ yield* i;
120
+ }
121
+ }
122
+
123
+ export async function *downloadAndExtract<T>(url: string): CSVParserIterator<T> {
124
+ const bodyStream = await getDownloadStream(url);
125
+ const fileEntries = unzipAndExtractZipBuffer(bodyStream);
126
+ for await (const entry of fileEntries) {
127
+ const csvParser = csvParse(entry, { quote: false });
128
+ let header: string[] | undefined = undefined;
129
+ for await (const r of csvParser) {
130
+ const record = r as string[];
131
+ // save header
132
+ if (typeof header === 'undefined') {
133
+ header = record;
134
+ continue;
135
+ }
136
+ yield record.reduce<Record<string, string>>((acc, value, index) => {
137
+ acc[header![index]] = value;
138
+ return acc;
139
+ }, {}) as T;
140
+ }
141
+ }
142
+ }
143
+
144
+ export async function *getAndStreamCSVDataForId<T = Record<string, string>>(id: string): CSVParserIterator<T> {
145
+ const res = await getCkanPackageById(id);
146
+ const url = getUrlForCSVResource(res);
147
+ if (!url) {
148
+ throw new Error('No CSV resource found');
149
+ }
150
+ const settings = await loadSettings();
151
+ for await (const record of downloadAndExtract<T>(url)) {
152
+ const lgCode = (record as {'lg_code': string})['lg_code'];
153
+ if (!lgCodeMatch(settings, lgCode)) { continue; }
154
+ yield record;
155
+ }
156
+ }
157
+
158
+ export async function getAndParseCSVDataForId<T = Record<string, string>>(id: string): Promise<T[]> {
159
+ return Array.fromAsync(getAndStreamCSVDataForId<T>(id));
160
+ }
161
+
162
+ export function findResultByTypeAndArea(results: CKANPackageSearchResult[], dataType: string, area: string): CKANPackageSearchResult | undefined {
163
+ return results.find((result) => (
164
+ result.extras.findIndex((extra) => (extra.key === "データセット種別" && extra.value === dataType)) > 0 &&
165
+ result.extras.findIndex((extra) => (extra.key === "対象地域" && extra.value === area)) > 0
166
+ ));
167
+ }
src/lib/ckan_data/chiban.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type ChibanData = {
2
+ /// 全国地方公共団体コード
3
+ lg_code: string
4
+ /// 町字ID
5
+ machiaza_id: string
6
+ /// 地番ID
7
+ prc_id: string
8
+ /// 市区町村名
9
+ city: string
10
+ /// 政令市区名
11
+ ward: string
12
+ /// 大字・町名
13
+ oaza_cho: string
14
+ /// 丁目名
15
+ chome: string
16
+ /// 小字名
17
+ koaza: string
18
+ /// 地番1
19
+ prc_num1: string
20
+ /// 地番2
21
+ prc_num2: string
22
+ /// 地番3
23
+ prc_num3: string
24
+ /// 住居表示フラグ
25
+ rsdt_addr_flg: string
26
+ /// 地番レコード区分フラグ
27
+ prc_rec_flg: string
28
+ /// 地番区域コード
29
+ prc_area_code: string
30
+ /// 効力発生日
31
+ efct_date: string
32
+ /// 廃止日
33
+ ablt_date: string
34
+ /// 原典資料コード
35
+ src_code: string
36
+ /// 備考
37
+ remarks: string
38
+ /// 不動産番号
39
+ real_prop_num: string
40
+ };
41
+
42
+ export type ChibanPosData = {
43
+ /// 全国地方公共団体コード
44
+ lg_code: string;
45
+ /// 町字ID
46
+ machiaza_id: string;
47
+ /// 地番ID
48
+ prc_id: string;
49
+ /// 代表点_経度
50
+ rep_lon: string;
51
+ /// 代表点_緯度
52
+ rep_lat: string;
53
+ /// 代表点_座標参照系
54
+ rep_srid: string;
55
+ /// 代表点_地図情報レベル
56
+ rep_scale: string;
57
+ /// 代表点_原典資料コード
58
+ rep_src_code: string;
59
+ /// ポリゴン_ファイル名
60
+ plygn_fname: string;
61
+ /// ポリゴン_キーコード
62
+ plygn_kcode: string;
63
+ /// ポリゴン_データフォーマット
64
+ plygn_fmt: string;
65
+ /// ポリゴン_座標参照系
66
+ plygn_srid: string;
67
+ /// ポリゴン_地図情報レベル
68
+ plygn_scale: string;
69
+ /// ポリゴン_原典資料コード
70
+ plygn_src_code: string;
71
+ /// 法務省地図_市区町村コード
72
+ moj_map_city_code: string;
73
+ /// 法務省地図_大字コード
74
+ moj_map_oaza_code: string;
75
+ /// 法務省地図_丁目コード
76
+ moj_map_chome_code: string;
77
+ /// 法務省地図_小字コード
78
+ moj_map_koaza_code: string;
79
+ /// 法務省地図_予備コード
80
+ moj_map_spare_code: string;
81
+ /// 法務省地図_筆id
82
+ moj_map_brushid: string;
83
+ };
src/lib/ckan_data/city.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type CityData = {
2
+ /// 全国地方公共団体コード
3
+ lg_code: string;
4
+ /// 都道府県名
5
+ pref: string;
6
+ /// 都道府県名_カナ
7
+ pref_kana: string;
8
+ /// 都道府県名_英字
9
+ pref_roma: string;
10
+ /// 郡名
11
+ county: string;
12
+ /// 郡名_カナ
13
+ county_kana: string;
14
+ /// 郡名_英字
15
+ county_roma: string;
16
+ /// 市区町村名
17
+ city: string;
18
+ /// 市区町村名_カナ
19
+ city_kana: string;
20
+ /// 市区町村名_英字
21
+ city_roma: string;
22
+ /// 政令市区名
23
+ ward: string;
24
+ /// 政令市区名_カナ
25
+ ward_kana: string;
26
+ /// 政令市区名_英字
27
+ ward_roma: string;
28
+ /// 効力発生日
29
+ efct_date: string;
30
+ /// 廃止日
31
+ ablt_date: string;
32
+ /// 備考
33
+ remarks: string;
34
+ };
35
+
36
+ export type CityPosData = {
37
+ /// 全国地方公共団体コード
38
+ lg_code: string;
39
+ /// 代表点_経度
40
+ rep_lon: string;
41
+ /// 代表点_緯度
42
+ rep_lat: string;
43
+ /// 代表点_座標参照系
44
+ rep_srid: string;
45
+ /// 代表点_地図情報レベル
46
+ rep_scale: string;
47
+ /// ポリゴン_ファイル名
48
+ plygn_fname: string;
49
+ /// ポリゴン_キーコード
50
+ plygn_kcode: string;
51
+ /// ポリゴン_データフォーマット
52
+ plygn_fmt: string;
53
+ /// ポリゴン_座標参照系
54
+ plygn_srid: string;
55
+ /// ポリゴン_地図情報レベル
56
+ plygn_scale: string;
57
+ };
58
+
59
+ export type CityDataWithPos = CityData & CityPosData;
60
+
61
+ export function mergeCityData(cityData: CityData[], cityPosData: CityPosData[]): CityDataWithPos[] {
62
+ const out: CityDataWithPos[] = [];
63
+ for (const city of cityData) {
64
+ const pos = cityPosData.find((pos) => pos.lg_code === city.lg_code);
65
+ if (pos) {
66
+ out.push({ ...city, ...pos });
67
+ }
68
+ }
69
+ return out;
70
+ }
src/lib/ckan_data/index.test.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test, { describe } from 'node:test';
3
+
4
+ import * as index from './index.js';
5
+
6
+ await describe('ckan_data/index', async () => {
7
+ await describe('joinAsyncIterators', async () => {
8
+ await test('it correctly joins two async iterators when they are ordered', async () => {
9
+ const one = async function*(){
10
+ await new Promise((resolve) => setTimeout(resolve, 10));
11
+ yield *[
12
+ { id: 100, name: 'Alice' },
13
+ { id: 101, name: 'Bob' }
14
+ ];
15
+ };
16
+ const two = async function*(){
17
+ await new Promise((resolve) => setTimeout(resolve, 10));
18
+ yield *[
19
+ { id: 100, age: 500 },
20
+ { id: 101, age: 501 }
21
+ ];
22
+ };
23
+
24
+ const res = await Array.fromAsync(
25
+ index.mergeDataLeftJoin(one(), two(), ['id'])
26
+ );
27
+
28
+ assert.deepStrictEqual(res, [
29
+ { id: 100, name: 'Alice', age: 500 },
30
+ { id: 101, name: 'Bob', age: 501 },
31
+ ]);
32
+ });
33
+
34
+ await test('it correctly joins two async iterators when they are out of order', async () => {
35
+ const one = async function *() {
36
+ await new Promise((resolve) => setTimeout(resolve, 10));
37
+ yield *[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }];
38
+ };
39
+ const two = async function *() {
40
+ await new Promise((resolve) => setTimeout(resolve, 10));
41
+ yield *[{ id: 2, age: 30 }, { id: 1, age: 25 }, { id: 4, age: 35 }];
42
+ };
43
+
44
+ const res = await Array.fromAsync(
45
+ index.mergeDataLeftJoin(one(), two(), ['id'])
46
+ );
47
+
48
+ assert.deepStrictEqual(res, [
49
+ { id: 1, name: 'Alice', age: 25 },
50
+ { id: 2, name: 'Bob', age: 30 },
51
+ { id: 3, name: 'Charlie' },
52
+ ]);
53
+ });
54
+ });
55
+ });
src/lib/ckan_data/index.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Database from "better-sqlite3";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { pipeline } from "node:stream/promises";
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ function _createKey(data: any, keys: string[]): string {
9
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
10
+ return keys.map((key) => `${data[key]}`).join("|");
11
+ }
12
+
13
+ export async function *mergeDataLeftJoin<T, U>(left: AsyncIterableIterator<T>, right: AsyncIterableIterator<U>, keys: string[], memory: boolean = false): AsyncIterableIterator<(T | T & U)> {
14
+ let tmpDbPath = ":memory:";
15
+
16
+ if (!memory) {
17
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "merge-data-left-join-"));
18
+ tmpDbPath = path.join(tmpDir, "db.sqlite3");
19
+ console.log(`Creating temporary database: ${tmpDbPath}`);
20
+ }
21
+
22
+ const db = new Database(tmpDbPath);
23
+ db.pragma("synchronous = OFF");
24
+ db.pragma("journal_mode = MEMORY");
25
+ db.exec(`
26
+ CREATE TABLE l (
27
+ key TEXT,
28
+ data JSONB
29
+ );
30
+ CREATE TABLE r (
31
+ key TEXT,
32
+ data JSONB
33
+ );
34
+ `);
35
+ const stmt1 = db.prepare("INSERT INTO l VALUES (?, ?)");
36
+ const stmt2 = db.prepare("INSERT INTO r VALUES (?, ?)");
37
+
38
+ await Promise.all([
39
+ pipeline(left, async function (source) {
40
+ for await (const data of source) {
41
+ stmt1.run(_createKey(data, keys), JSON.stringify(data));
42
+ }
43
+ }),
44
+ pipeline(right, async function (source) {
45
+ for await (const data of source) {
46
+ stmt2.run(_createKey(data, keys), JSON.stringify(data));
47
+ }
48
+ }),
49
+ ]);
50
+ db.exec(`
51
+ CREATE INDEX l_key ON l(key);
52
+ CREATE INDEX r_key ON r(key);
53
+ `);
54
+
55
+ const select = db.prepare<void[], {d01: string, d02: string}>(`
56
+ SELECT
57
+ json_patch(l.data, coalesce(r.data, '{}')) AS d01
58
+ FROM
59
+ l
60
+ LEFT JOIN r ON l.key = r.key
61
+ `);
62
+ for (const data of select.iterate()) {
63
+ yield JSON.parse(data.d01);
64
+ }
65
+
66
+ db.close();
67
+ if (!memory) {
68
+ await fs.rm(path.dirname(tmpDbPath), { recursive: true });
69
+ }
70
+ }
src/lib/ckan_data/machi_aza.ts ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type MachiAzaData = {
2
+ /// 全国地方公共団体コード
3
+ lg_code: string;
4
+ /// 町字ID
5
+ machiaza_id: string;
6
+ /// 町字区分コード
7
+ machiaza_type: string;
8
+ /// 都道府県名
9
+ pref: string;
10
+ /// 都道府県名_カナ
11
+ pref_kana: string;
12
+ /// 都道府県名_英字
13
+ pref_roma: string;
14
+ /// 郡名
15
+ county: string;
16
+ /// 郡名_カナ
17
+ county_kana: string;
18
+ /// 郡名_英字
19
+ county_roma: string;
20
+ /// 市区町村名
21
+ city: string;
22
+ /// 市区町村名_カナ
23
+ city_kana: string;
24
+ /// 市区町村名_英字
25
+ city_roma: string;
26
+ /// 政令市区名
27
+ ward: string;
28
+ /// 政令市区名_カナ
29
+ ward_kana: string;
30
+ /// 政令市区名_英字
31
+ ward_roma: string;
32
+ /// 大字・町名
33
+ oaza_cho: string;
34
+ /// 大字・町名_カナ
35
+ oaza_cho_kana: string;
36
+ /// 大字・町名_英字
37
+ oaza_cho_roma: string;
38
+ /// 丁目名
39
+ chome: string;
40
+ /// 丁目名_カナ
41
+ chome_kana: string;
42
+ /// 丁目名_数字
43
+ chome_number: string;
44
+ /// 小字名
45
+ koaza: string;
46
+ /// 小字名_カナ
47
+ koaza_kana: string;
48
+ /// 小字名_英字
49
+ koaza_roma: string;
50
+ /// 同一町字識別情報
51
+ machiaza_dist: string;
52
+ /// 住居表示フラグ
53
+ rsdt_addr_flg: string;
54
+ /// 住居表示方式コード
55
+ rsdt_addr_mtd_code: string;
56
+ /// 大字・町名_通称フラグ
57
+ oaza_cho_aka_flg: string;
58
+ /// 小字名_通称コード
59
+ koaza_aka_code: string;
60
+ /// 大字・町名_電子国土基本図外字
61
+ oaza_cho_gsi_uncmn: string;
62
+ /// 小字名_電子国土基本図外字
63
+ koaza_gsi_uncmn: string;
64
+ /// 状態フラグ
65
+ status_flg: string;
66
+ /// 起番フラグ
67
+ wake_num_flg: string;
68
+ /// 効力発生日
69
+ efct_date: string;
70
+ /// 廃止日
71
+ ablt_date: string;
72
+ /// 原典資料コード
73
+ src_code: string;
74
+ /// 郵便番号
75
+ post_code: string;
76
+ /// 備考
77
+ remarks: string;
78
+ };
79
+
80
+ export type MachiAzaPosData = {
81
+ /// 全国地方公共団体コード
82
+ lg_code: string;
83
+ /// 町字ID
84
+ machiaza_id: string;
85
+ /// 住居表示フラグ
86
+ rsdt_addr_flg: string;
87
+ /// 代表点_経度
88
+ rep_lon: string;
89
+ /// 代表点_緯度
90
+ rep_lat: string;
91
+ /// 代表点_座標参照系
92
+ rep_srid: string;
93
+ /// 代表点_地図情報レベル
94
+ rep_scale: string;
95
+ /// 代表点_原典資料コード
96
+ rep_src_code: string;
97
+ /// ポリゴン_ファイル名
98
+ plygn_fname: string;
99
+ /// ポリゴン_キーコード
100
+ plygn_kcode: string;
101
+ /// ポリゴン_データフォーマット
102
+ plygn_fmt: string;
103
+ /// ポリゴン_座標参照系
104
+ plygn_srid: string;
105
+ /// ポリゴン_地図情報レベル
106
+ plygn_scale: string;
107
+ /// ポリゴン_原典資料コード
108
+ plygn_src_code: string;
109
+ /// 位置参照情報_大字町丁目コード
110
+ pos_oaza_cho_chome_code: string;
111
+ /// 位置参照情報_データ整備年度
112
+ pos_data_mnt_year: string;
113
+ /// 国勢調査_境界_小地域(町丁・字等別)_KEY_CODE
114
+ cns_bnd_s_area_kcode: string;
115
+ /// 国勢調査_境界_データ整備年度
116
+ cns_bnd_year: string;
117
+ };
src/lib/ckan_data/prefecture.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type PrefData = {
2
+ /// 全国地方公共団体コード
3
+ lg_code: string;
4
+ /// 都道府県名
5
+ pref: string;
6
+ /// 都道府県名_カナ
7
+ pref_kana: string;
8
+ /// 都道府県名_英字
9
+ pref_roma: string;
10
+ /// 効力発生日
11
+ efct_date: string;
12
+ /// 廃止日
13
+ ablt_date: string;
14
+ /// 備考
15
+ remarks: string;
16
+ };
17
+
18
+ export type PrefPosData = {
19
+ /// 全国地方公共団体コード
20
+ lg_code: string;
21
+ /// 代表点_経度
22
+ rep_lon: string;
23
+ /// 代表点_緯度
24
+ rep_lat: string;
25
+ /// 代表点_座標参照系
26
+ rep_srid: string;
27
+ /// 代表点_地図情報レベル
28
+ rep_scale: string;
29
+ /// ポリゴン_ファイル名
30
+ plygn_fname: string;
31
+ /// ポリゴン_キーコード
32
+ plygn_kcode: string;
33
+ /// ポリゴン_データフォーマット
34
+ plygn_fmt: string;
35
+ /// ポリゴン_座標参照系
36
+ plygn_srid: string;
37
+ /// ポリゴン_地図情報レベル
38
+ plygn_scale: string;
39
+ };
40
+
41
+ export type PrefDataWithPos = PrefData & PrefPosData;
42
+
43
+ export function mergePrefectureData(prefData: PrefData[], prefPosData: PrefPosData[]): PrefDataWithPos[] {
44
+ const out: PrefDataWithPos[] = [];
45
+ for (const pref of prefData) {
46
+ const pos = prefPosData.find((pos) => pos.lg_code === pref.lg_code);
47
+ if (pos) {
48
+ out.push({ ...pref, ...pos });
49
+ }
50
+ }
51
+ return out;
52
+ }
src/lib/ckan_data/rsdtdsp_rsdt.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mergeDataLeftJoin } from "./index.js";
2
+
3
+ export type RsdtdspRsdtData = {
4
+ /// 全国地方公共団体コード
5
+ lg_code: string;
6
+ /// 町字ID
7
+ machiaza_id: string;
8
+ /// 街区ID
9
+ blk_id: string;
10
+ /// 住居ID
11
+ rsdt_id: string;
12
+ /// 住居2ID
13
+ rsdt2_id: string;
14
+ /// 市区町村名
15
+ city: string;
16
+ /// 政令市区名
17
+ ward: string;
18
+ /// 大字・町名
19
+ oaza_cho: string;
20
+ /// 丁目名
21
+ chome: string;
22
+ /// 小字名
23
+ koaza: string;
24
+ /// 街区符号
25
+ blk_num: string;
26
+ /// 住居番号
27
+ rsdt_num: string;
28
+ /// 住居番号2
29
+ rsdt_num2: string;
30
+ /// 基礎番号・住居番号区分
31
+ basic_rsdt_div: string;
32
+ /// 住居表示フラグ
33
+ rsdt_addr_flg: string;
34
+ /// 住居表示方式コード
35
+ rsdt_addr_mtd_code: string;
36
+ /// 状態フラグ
37
+ status_flg: string;
38
+ /// 効力発生日
39
+ efct_date: string;
40
+ /// 廃止日
41
+ ablt_date: string;
42
+ /// 原典資料コード
43
+ src_code: string;
44
+ /// 備考
45
+ remarks: string;
46
+ };
47
+
48
+ export type RsdtdspRsdtPosData = {
49
+ /// 全国地方公共団体コード
50
+ lg_code: string;
51
+ /// 町字ID
52
+ machiaza_id: string;
53
+ /// 街区ID
54
+ blk_id: string;
55
+ /// 住居ID
56
+ rsdt_id: string;
57
+ /// 住居2ID
58
+ rsdt2_id: string;
59
+ /// 住居表示フラグ
60
+ rsdt_addr_flg: string;
61
+ /// 住居表示方式コード
62
+ rsdt_addr_mtd_code: string;
63
+ /// 代表点_経度
64
+ rep_lon: string;
65
+ /// 代表点_緯度
66
+ rep_lat: string;
67
+ /// 代表点_座標参照系
68
+ rep_srid: string;
69
+ /// 代表点_地図情報レベル
70
+ rep_scale: string;
71
+ /// 代表点_原典資料コード
72
+ rep_src_code: string;
73
+ /// 電子国土基本図(地名情報)「住居表示住所」_住所コード(可読)
74
+ rsdt_addr_code_rdbl: string;
75
+ /// 電子国土基本図(地名情報)「住居表示住所」_データ整備日
76
+ rsdt_addr_data_mnt_date: string;
77
+ /// 基礎番号・住居番号区分
78
+ basic_rsdt_div: string;
79
+ };
80
+
81
+ export type RsdtdspRsdtDataWithPos = RsdtdspRsdtData | RsdtdspRsdtData & RsdtdspRsdtPosData;
82
+
83
+ export function mergeRsdtdspRsdtData(
84
+ rsdtdspRsdtData: AsyncIterableIterator<RsdtdspRsdtData>,
85
+ rsdtdspRsdtPosData: AsyncIterableIterator<RsdtdspRsdtPosData>
86
+ ): AsyncIterableIterator<RsdtdspRsdtDataWithPos> {
87
+ return mergeDataLeftJoin(rsdtdspRsdtData, rsdtdspRsdtPosData, ["lg_code", "machiaza_id", "blk_id", "rsdt_id", "rsdt2_id"]);
88
+ }
src/lib/fetch_tools.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ import { fetch } from 'undici';
5
+
6
+ const USER_AGENT = 'curl/8.7.1';
7
+ const CACHE_DIR = path.join(import.meta.dirname, '..', '..', 'cache');
8
+
9
+ export async function getDownloadStream(url: string): Promise<Buffer> {
10
+ const cacheKey = url.replace(/[^a-zA-Z0-9]/g, '_');
11
+ const cacheFile = path.join(CACHE_DIR, 'files', cacheKey);
12
+
13
+ let buffer: Buffer;
14
+ if (!fs.existsSync(cacheFile)) {
15
+ // console.log(`Downloading ${url}`);
16
+ const res = await fetch(url, {
17
+ headers: {
18
+ 'User-Agent': USER_AGENT,
19
+ },
20
+ });
21
+
22
+ if (!res.ok) {
23
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
24
+ }
25
+
26
+ await fs.promises.mkdir(path.dirname(cacheFile), { recursive: true });
27
+ const body = await res.arrayBuffer();
28
+ buffer = Buffer.from(body);
29
+ await fs.promises.writeFile(cacheFile, buffer);
30
+ } else {
31
+ buffer = await fs.promises.readFile(cacheFile);
32
+ }
33
+
34
+ return buffer;
35
+ }
src/lib/mlit_nlftp.test.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ downloadAndExtractNlftpMlitFile,
6
+ } from './mlit_nlftp.js';
7
+
8
+ await test.describe('downloadAndExtractNlftpMlitFile', async () => {
9
+ await test('it works', async () => {
10
+ // 沖縄県
11
+ const data = await downloadAndExtractNlftpMlitFile('47');
12
+ assert.strictEqual(data.length, 1228);
13
+ assert.strictEqual(data[0].machiaza_id, 'MLIT:472010001001');
14
+ assert.strictEqual(data[0].pref_name, '沖縄県');
15
+ assert.strictEqual(data[0].city_name, '那覇市');
16
+ assert.strictEqual(data[0].oaza_cho, '古波蔵');
17
+ assert.strictEqual(data[0].chome, '一丁目');
18
+ assert.strictEqual(data[0].point.length, 2);
19
+ });
20
+ });
src/lib/mlit_nlftp.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parse as csvParse } from 'csv-parse';
2
+ import iconv from 'iconv-lite';
3
+
4
+ import { unzipAndExtractZipBuffer } from './zip_tools.js';
5
+ import { getDownloadStream } from './fetch_tools.js';
6
+
7
+ const SKIP_ROWS = new Set([
8
+ // 国土数値情報では「柿さき町」で登録されているが、ABRには「柿崎町」として登録されている。
9
+ // 柿崎町を優先するので、「柿さき町」はスキップする。
10
+ '愛知県/安城市/柿さき町',
11
+ ]);
12
+
13
+ export type NlftpMlitDataRow = {
14
+ machiaza_id: string
15
+
16
+ pref_name: string
17
+ city_name: string
18
+
19
+ oaza_cho: string
20
+ chome?: string
21
+ point: [number, number]
22
+ }
23
+
24
+ // type NlftpMlitCsvRow = {
25
+ // 0 "都道府県コード": string
26
+ // 1 "都道府県名": string
27
+ // 2 "市区町村コード": string
28
+ // 3 "市区町村名": string
29
+ // 4 "大字町丁目コード": string
30
+ // 5 "大字町丁目名": string
31
+ // 6 "緯度": string
32
+ // 7 "経度": string
33
+ // 8 "原典資料コード": string
34
+ // 9 "大字・字・丁目区分コード": string
35
+ // }
36
+
37
+ function parseRows(rows: string[][]): NlftpMlitDataRow[] {
38
+ // remove header row
39
+ rows.shift();
40
+ // sort by code (should already be sorted, just in case)
41
+ rows.sort((a, b) => {
42
+ return a[4].localeCompare(b[4]);
43
+ });
44
+
45
+ const result: NlftpMlitDataRow[] = [];
46
+ for (const row of rows) {
47
+ if (SKIP_ROWS.has(`${row[1]}/${row[3]}/${row[5]}`)) continue;
48
+
49
+ let oaza_cho = row[5];
50
+ let chome: string | undefined = undefined;
51
+ const chomeMatch = oaza_cho.match(/^(.*?)([一二三四五六七八九十]+丁目)$/);
52
+ if (chomeMatch) {
53
+ oaza_cho = chomeMatch[1];
54
+ chome = chomeMatch[2];
55
+ }
56
+
57
+ result.push({
58
+ machiaza_id: `MLIT:${row[4]}`,
59
+ pref_name: row[1],
60
+ city_name: row[3],
61
+ oaza_cho,
62
+ chome,
63
+ point: [
64
+ parseFloat(row[7]), // longitude
65
+ parseFloat(row[6]), // latitude
66
+ ],
67
+ });
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * 国土数値情報からデータをダウンロードしパースします。
75
+ * 参照: https://nlftp.mlit.go.jp/isj/
76
+ */
77
+ export async function downloadAndExtractNlftpMlitFile(prefCode: string): Promise<NlftpMlitDataRow[]> {
78
+ const version = '17.0b'; // 大字・町丁目レベル位置参照情報
79
+ // 22.0a は街区レベル位置参照情報なので、ここには必要ない
80
+ const url = `https://nlftp.mlit.go.jp/isj/dls/data/${version}/${prefCode}000-${version}.zip`;
81
+ const bodyStream = await getDownloadStream(url);
82
+ const entries = unzipAndExtractZipBuffer(bodyStream);
83
+ for await (const entry of entries) {
84
+ if (entry.path.slice(-4) !== '.csv') continue;
85
+ const decoded = iconv.decode(entry, 'Shift_JIS');
86
+ const rows = await Array.fromAsync<string[]>(
87
+ csvParse(decoded)
88
+ );
89
+ return parseRows(rows);
90
+ }
91
+ throw new Error('No CSV file detected in archive file');
92
+ }
src/lib/proj.test.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import proj from './proj.js';
5
+
6
+ await test.describe('proj', async () => {
7
+ await test('should project coordinates from EPSG:6668 to EPSG:4326', () => {
8
+ const coords = [139.6917, 35.6895];
9
+ const projected = proj('EPSG:6668', 'EPSG:4326', coords);
10
+ assert.deepStrictEqual(projected, [139.6917, 35.6895]);
11
+ });
12
+ });
src/lib/proj.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import proj4 from "proj4";
2
+ import { LngLat } from "../data.js";
3
+
4
+ proj4.defs("EPSG:4612","+proj=longlat +ellps=GRS80 +no_defs +type=crs");
5
+ proj4.defs("EPSG:6668","+proj=longlat +ellps=GRS80 +no_defs +type=crs");
6
+
7
+ export default proj4;
8
+
9
+ type DataRowWithGeometry = {
10
+ rep_lon: string
11
+ rep_lat: string
12
+ rep_srid: string
13
+ };
14
+
15
+ /**
16
+ * Reprojects Address Base Registry lon/lat data to EPSG:4326
17
+ * @returns
18
+ */
19
+ export function projectABRData(dataRow: DataRowWithGeometry): LngLat {
20
+ const input: LngLat = [parseFloat(dataRow.rep_lon), parseFloat(dataRow.rep_lat)];
21
+ return proj4(dataRow.rep_srid, "EPSG:4326", input);
22
+ }
src/lib/settings.test.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test, { describe, before, after } from 'node:test';
3
+
4
+ import path from 'node:path';
5
+ import fs from 'node:fs/promises';
6
+
7
+ import * as settings from './settings.js';
8
+
9
+ const fixtureDir = path.join(import.meta.dirname, '..', '..', 'test', 'fixtures', 'lib', 'settings');
10
+
11
+ before(async () => {
12
+ await fs.copyFile(path.join(fixtureDir, 'settings.json'), path.join(process.cwd(), 'settings.json'));
13
+ });
14
+
15
+ after(async () => {
16
+ await fs.rm(path.join(process.cwd(), 'settings.json'));
17
+ delete process.env.SETTINGS_JSON;
18
+ delete process.env.SETTINGS_PATH;
19
+ });
20
+
21
+ await test('loadSettings', async () => {
22
+ const parsedSettings = await settings.loadSettings();
23
+ assert.equal(parsedSettings.lgCodes.length, 1);
24
+ assert.ok(parsedSettings.lgCodes[0].test('011002'));
25
+ assert.ok(!parsedSettings.lgCodes[0].test('131002'));
26
+ });
27
+
28
+ await describe('lgCodeMatch', async () => {
29
+ await test('basic settings', () => {
30
+ const settingsData = settings.parseSettings({
31
+ lgCodes: ['^01', '^13', '472018'],
32
+ });
33
+ assert.ok(settings.lgCodeMatch(settingsData, '011002'));
34
+ assert.ok(settings.lgCodeMatch(settingsData, '131002'));
35
+ assert.ok(!settings.lgCodeMatch(settingsData, '465054'));
36
+ assert.ok(settings.lgCodeMatch(settingsData, '472018'));
37
+ });
38
+
39
+ await test('市区町村まで指定されているときは、都道府県全体に対してマッチする', () => {
40
+ const settingsData = settings.parseSettings({
41
+ lgCodes: ['131002', '472018'],
42
+ });
43
+ assert.ok(settings.lgCodeMatch(settingsData, '131002'));
44
+ assert.ok(settings.lgCodeMatch(settingsData, '130001'));
45
+
46
+ assert.ok(settings.lgCodeMatch(settingsData, '472018'));
47
+ assert.ok(settings.lgCodeMatch(settingsData, '470007'));
48
+
49
+ assert.ok(!settings.lgCodeMatch(settingsData, '011002'));
50
+ assert.ok(!settings.lgCodeMatch(settingsData, '460001'));
51
+ });
52
+ });
53
+
54
+ await test('loadSettings with overwritten JSON', async () => {
55
+ process.env.SETTINGS_JSON = JSON.stringify({ lgCodes: ['^99'] });
56
+ const parsedSettings = await settings.loadSettings();
57
+ assert.equal(parsedSettings.lgCodes.length, 1);
58
+ assert.ok(parsedSettings.lgCodes[0].test('990000'));
59
+ });
src/lib/settings.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LRUCache } from 'lru-cache';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+
5
+ const settingsPath = () => {
6
+ if (process.env.SETTINGS_JSON) return "json:" + process.env.SETTINGS_JSON;
7
+ if (process.env.SETTINGS_PATH) return process.env.SETTINGS_PATH;
8
+ return path.join(process.cwd(), "settings.json");
9
+ }
10
+
11
+ /** settings.json */
12
+ type Settings = {
13
+ /**
14
+ * 出力する自治体のデータを制限するためのフィルター
15
+ * 全国地方公共団体コードをマッチする正規表現の文字列を配列で指定してください。
16
+ * OR条件で指定されたコードのいずれかに一致するデータのみ出力されます。
17
+ *
18
+ * 設定されていない場合は、全てのデータが出力されます。
19
+ *
20
+ * 例: ["011002", "012025"] は、北海道札幌市と北海道函館市のデータのみ出力されます。
21
+ * 例: ["^01"] は、北海道の全ての自治体のデータのみ出力されます。
22
+ */
23
+ lgCodes?: string[];
24
+ }
25
+
26
+ const DEFAULT_SETTINGS: Settings = {};
27
+
28
+ // ---
29
+
30
+ async function loadRawSettings(input: string): Promise<Settings> {
31
+ if (input.startsWith("json:")) {
32
+ return JSON.parse(input.slice(5)) as Settings;
33
+ }
34
+
35
+ try {
36
+ const settingsData = await fs.readFile(input, "utf-8");
37
+ return JSON.parse(settingsData) as Settings;
38
+ } catch (e) {
39
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") {
40
+ return DEFAULT_SETTINGS;
41
+ }
42
+ throw e;
43
+ }
44
+ }
45
+
46
+ export function parseSettings(settings: Settings): ParsedSettings {
47
+ return {
48
+ lgCodes: settings.lgCodes?.map((code) => new RegExp(code)) || [],
49
+ };
50
+ }
51
+
52
+ const settingsCache = new LRUCache<string, ParsedSettings>({
53
+ max: 10,
54
+ fetchMethod: async (key) => {
55
+ const rawSettings = await loadRawSettings(key);
56
+ return parseSettings(rawSettings);
57
+ },
58
+ });
59
+
60
+ export type ParsedSettings = {
61
+ lgCodes: RegExp[];
62
+ }
63
+
64
+ export async function loadSettings(): Promise<ParsedSettings> {
65
+ const settings = await settingsCache.fetch(settingsPath());
66
+ if (!settings) {
67
+ return { lgCodes: [] };
68
+ }
69
+ return settings;
70
+ }
71
+
72
+ export function lgCodeMatch(settings: ParsedSettings, lgCode: string): boolean {
73
+ if (settings.lgCodes.length === 0) {
74
+ return true;
75
+ }
76
+ for (const re of settings.lgCodes) {
77
+ if (re.test(lgCode)) {
78
+ return true;
79
+ }
80
+
81
+ // re が市区町村まで指定されている場合は、都道府県全体に対してマッチする
82
+ const cityCodeMatch = re.source.match(/^\^?(\d{2})\d{3}/);
83
+ if (cityCodeMatch) {
84
+ const prefCode = cityCodeMatch[1];
85
+ if (lgCode.startsWith(prefCode + '000')) {
86
+ return true;
87
+ }
88
+ }
89
+ }
90
+ return false;
91
+ }
src/lib/zip_tools.test.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import path from 'node:path';
5
+ import fs from 'node:fs';
6
+
7
+ import * as zip_tools from './zip_tools.js';
8
+
9
+ const fixtureDir = path.join(import.meta.dirname, '..', '..', 'test', 'fixtures', 'lib', 'zip_tools');
10
+
11
+ await test.describe('unzipAndExtractZipFile', async () => {
12
+ await test('it works for a single layer of zip', async () => {
13
+ const filePath = path.join(fixtureDir, 'single_level.csv.zip');
14
+ const stream = fs.createReadStream(filePath);
15
+ const files = await Array.fromAsync(zip_tools.unzipAndExtractZipFile(stream));
16
+ assert.strictEqual(files.length, 1);
17
+ const file0 = Buffer.concat(await Array.fromAsync(files[0])).toString('utf-8');
18
+ assert.strictEqual(file0, `It,works!\n\n`);
19
+ });
20
+
21
+ await test('it works for a double layer of zip', async () => {
22
+ const filePath = path.join(fixtureDir, 'double_level.csv.zip');
23
+ const stream = fs.createReadStream(filePath);
24
+ const files = await Array.fromAsync(zip_tools.unzipAndExtractZipFile(stream));
25
+ assert.strictEqual(files.length, 2);
26
+
27
+ const file0 = Buffer.concat(await Array.fromAsync(files[0])).toString('utf-8');
28
+ assert.strictEqual(file0, `It,works!\nDouble,1\n`);
29
+
30
+ const file1 = Buffer.concat(await Array.fromAsync(files[1])).toString('utf-8');
31
+ assert.strictEqual(file1, `It,works!\nDouble,2\n`);
32
+ });
33
+ });
34
+
35
+ await test.describe('unzipAndExtractZipBuffer', async () => {
36
+ await test('it works for a single layer of zip', async () => {
37
+ const filePath = path.join(fixtureDir, 'single_level.csv.zip');
38
+ const buffer = await fs.promises.readFile(filePath);
39
+ const files = await Array.fromAsync(zip_tools.unzipAndExtractZipBuffer(buffer));
40
+ assert.strictEqual(files.length, 1);
41
+ const file0 = files[0].toString('utf-8');
42
+ assert.strictEqual(file0, `It,works!\n\n`);
43
+ });
44
+
45
+ await test('it works for a double layer of zip', async () => {
46
+ const filePath = path.join(fixtureDir, 'double_level.csv.zip');
47
+ const buffer = await fs.promises.readFile(filePath);
48
+ const files = await Array.fromAsync(zip_tools.unzipAndExtractZipBuffer(buffer));
49
+ assert.strictEqual(files.length, 2);
50
+
51
+ const file0 = files[0].toString('utf-8');
52
+ assert.strictEqual(file0, `It,works!\nDouble,1\n`);
53
+
54
+ const file1 = files[1].toString('utf-8');
55
+ assert.strictEqual(file1, `It,works!\nDouble,2\n`);
56
+ });
57
+ });
src/lib/zip_tools.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Readable } from 'node:stream';
2
+ import unzipper, { Entry } from 'unzipper';
3
+
4
+ /**
5
+ * A function to unzip a file that may or may not contain another zip file.
6
+ * The zip file is extracted and each file is returned as a readable stream.
7
+ */
8
+ export async function *unzipAndExtractZipFile(zipFile: Readable): AsyncGenerator<Entry> {
9
+ const files = zipFile.pipe(unzipper.Parse({forceStream: true}));
10
+ for await (const entry_ of files) {
11
+ const entry = entry_ as Entry;
12
+ if (entry.type === 'File' && entry.path.endsWith('.zip')) {
13
+ yield *unzipAndExtractZipFile(entry);
14
+ } else if (entry.type === 'File' && entry.path.endsWith('.csv')) {
15
+ yield entry;
16
+ } else {
17
+ entry.autodrain();
18
+ }
19
+ }
20
+ }
21
+
22
+ export async function *unzipAndExtractZipBuffer(zipFile: Buffer): AsyncGenerator<Buffer & {path: string}> {
23
+ const directory = await unzipper.Open.buffer(zipFile);
24
+ for (const file of directory.files) {
25
+ if (file.type === 'File' && file.path.endsWith('.zip')) {
26
+ const content = await file.buffer();
27
+ yield *unzipAndExtractZipBuffer(content);
28
+ } else if (file.type === 'File' && file.path.endsWith('.csv')) {
29
+ const content = await file.buffer();
30
+ yield Object.assign(content, {path: file.path});
31
+ }
32
+ }
33
+ }
src/processes/01_make_prefecture_city.test.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import fs from 'node:fs/promises';
5
+ import main from './01_make_prefecture_city.js';
6
+ import { PrefectureApi } from '../data.js';
7
+
8
+ await test.describe('with Hokkaido filter', async () => {
9
+ test.before(() => {
10
+ process.env.SETTINGS_JSON = JSON.stringify({ lgCodes: ['^01'] });
11
+ });
12
+
13
+ test.after(() => {
14
+ delete process.env.SETTINGS_JSON;
15
+ });
16
+
17
+ await test('it generates the API', async () => {
18
+ await fs.rm('./out/api_hokkaido', { recursive: true, force: true });
19
+ await main(['', '', './out/api_hokkaido']);
20
+ assert.ok(true);
21
+
22
+ const ja = JSON.parse(await fs.readFile('./out/api_hokkaido/ja.json', 'utf-8')) as PrefectureApi;
23
+ const jaData = ja.data;
24
+ assert.equal(jaData.length, 1);
25
+ const hokkaido = jaData[0];
26
+ assert.equal(hokkaido.pref, '北海道');
27
+ const cities = hokkaido.cities;
28
+ assert.equal(cities.length, 194);
29
+ assert.equal(cities[0].city, '札幌市');
30
+ });
31
+ });
32
+
src/processes/01_make_prefecture_city.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { getAndParseCSVDataForId } from '../lib/ckan.js';
5
+ import { CityApi, PrefectureApi, SingleCity, SinglePrefecture } from '../data.js';
6
+ import { projectABRData } from '../lib/proj.js';
7
+ import { CityData, CityPosData, mergeCityData } from '../lib/ckan_data/city.js';
8
+ import { mergePrefectureData, PrefData, PrefPosData } from '../lib/ckan_data/prefecture.js';
9
+
10
+ function outputCityData(outDir: string, prefName: string, apiData: CityApi, prefectureApi: PrefectureApi) {
11
+ // 政令都市の「区名」が無い場合は出力から除外する
12
+ const filteredApiData = apiData.data.filter((city) => {
13
+ return (
14
+ // 区名がある場合はそのまま出力
15
+ city.ward !== undefined ||
16
+ // 区名が無い場合は、同じ市区町村名で区名があるデータが無いか確認(ある=政令都市のため、出力しない。ない=政令都市ではない)
17
+ apiData.data.filter((c2) => c2.city === city.city).every((c2) => c2.ward === undefined)
18
+ );
19
+ });
20
+ apiData.data = filteredApiData;
21
+
22
+ prefectureApi.data.find((pref) => pref.pref === prefName)!.cities = filteredApiData;
23
+
24
+ const outFile = path.join(outDir, 'ja', `${prefName}.json`);
25
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
26
+ fs.writeFileSync(outFile, JSON.stringify(apiData));
27
+ console.log(`${prefName.padEnd(4, ' ')}: ${filteredApiData.length.toString(10).padEnd(3, ' ')} 件の市区町村を出力した`);
28
+ }
29
+
30
+ async function main(argv: string[]) {
31
+ const updated = Math.floor(Date.now() / 1000);
32
+ const outDir = argv[2] || path.join(import.meta.dirname, '..', '..', 'out', 'api');
33
+ fs.mkdirSync(outDir, { recursive: true });
34
+
35
+ const [
36
+ prefMain,
37
+ prefPos,
38
+
39
+ main,
40
+ pos,
41
+ ] = await Promise.all([
42
+ getAndParseCSVDataForId<PrefData>('ba-o1-000000_g2-000001'), // 都道府県
43
+ getAndParseCSVDataForId<PrefPosData>('ba-o1-000000_g2-000012'), // 位置参照拡張
44
+
45
+ getAndParseCSVDataForId<CityData>('ba-o1-000000_g2-000002'), // 市区町村
46
+ getAndParseCSVDataForId<CityPosData>('ba-o1-000000_g2-000013'), // 位置参照拡張
47
+ ]);
48
+ const rawData = mergeCityData(main, pos);
49
+
50
+ const prefApiData: SinglePrefecture[] = [];
51
+ const rawPrefData = mergePrefectureData(prefMain, prefPos);
52
+
53
+ for (const raw of rawPrefData) {
54
+ prefApiData.push({
55
+ code: parseInt(raw.lg_code),
56
+ pref: raw.pref,
57
+ pref_k: raw.pref_kana,
58
+ pref_r: raw.pref_roma,
59
+ point: projectABRData(raw),
60
+ cities: [],
61
+ });
62
+ }
63
+
64
+ const prefApi: PrefectureApi = {
65
+ meta: {
66
+ updated,
67
+ },
68
+ data: prefApiData,
69
+ };
70
+
71
+ let lastPref: string | undefined = undefined;
72
+ let allCount = 0;
73
+ const processedLgCodes: Set<string> = new Set();
74
+ let apiData: SingleCity[] = [];
75
+ for (const raw of rawData) {
76
+ allCount++;
77
+ if (lastPref !== raw.pref && lastPref !== undefined) {
78
+ const api: CityApi = {
79
+ meta: {
80
+ updated,
81
+ },
82
+ data: apiData,
83
+ };
84
+ outputCityData(outDir, lastPref, api, prefApi);
85
+ apiData = [];
86
+ }
87
+ if (lastPref !== raw.pref) {
88
+ lastPref = raw.pref;
89
+ }
90
+ apiData.push({
91
+ code: parseInt(raw.lg_code),
92
+ county: raw.county === '' ? undefined : raw.county,
93
+ county_k: raw.county_kana === '' ? undefined : raw.county_kana,
94
+ county_r: raw.county_roma === '' ? undefined : raw.county_roma,
95
+ city: raw.city,
96
+ city_k: raw.city_kana,
97
+ city_r: raw.city_roma,
98
+ ward: raw.ward === '' ? undefined : raw.ward,
99
+ ward_k: raw.ward_kana === '' ? undefined : raw.ward_kana,
100
+ ward_r: raw.ward_roma === '' ? undefined : raw.ward_roma,
101
+ point: projectABRData(raw),
102
+ });
103
+ processedLgCodes.add(raw.lg_code);
104
+ }
105
+ if (lastPref) {
106
+ const api: CityApi = {
107
+ meta: {
108
+ updated,
109
+ },
110
+ data: apiData,
111
+ };
112
+ outputCityData(outDir, lastPref, api, prefApi);
113
+ }
114
+
115
+ const outFile = path.join(outDir, 'ja.json');
116
+ fs.writeFileSync(outFile, JSON.stringify(prefApi));
117
+
118
+ console.log(`全国: ${allCount} ${processedLgCodes.size} 件の市区町村を出力した`);
119
+ }
120
+
121
+ export default main;
src/processes/02_machi_aza.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SingleMachiAza } from "../data.js";
2
+ import { MachiAzaData, MachiAzaPosData } from "../lib/ckan_data/machi_aza.js";
3
+ import { projectABRData } from "../lib/proj.js";
4
+
5
+ export function rawToMachiAza(raw: MachiAzaData | (MachiAzaData & MachiAzaPosData)): SingleMachiAza {
6
+ return {
7
+ machiaza_id: raw.machiaza_id,
8
+
9
+ oaza_cho: raw.oaza_cho === '' ? undefined : raw.oaza_cho,
10
+ oaza_cho_k: raw.oaza_cho_kana === '' ? undefined : raw.oaza_cho_kana,
11
+ oaza_cho_r: raw.oaza_cho_roma === '' ? undefined : raw.oaza_cho_roma,
12
+
13
+ chome: raw.chome === '' ? undefined : raw.chome,
14
+ chome_n: raw.chome_number === '' ? undefined : parseInt(raw.chome_number, 10),
15
+
16
+ koaza: raw.koaza === '' ? undefined : raw.koaza,
17
+ koaza_k: raw.koaza_kana === '' ? undefined : raw.koaza_kana,
18
+ koaza_r: raw.koaza_roma === '' ? undefined : raw.koaza_roma,
19
+
20
+ rsdt: raw.rsdt_addr_flg === '1' ? true : undefined,
21
+ point: 'rep_srid' in raw ? projectABRData(raw) : undefined,
22
+ };
23
+ }
src/processes/02_make_machi_aza.test.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import fs from 'node:fs/promises';
5
+ import main from './02_make_machi_aza.js';
6
+ import { MachiAzaApi } from '../data.js';
7
+
8
+ await test.describe('with filter for 452092 (宮崎県えびの市)', async () => {
9
+ test.before(() => {
10
+ process.env.SETTINGS_JSON = JSON.stringify({ lgCodes: ['452092'] });
11
+ });
12
+
13
+ test.after(() => {
14
+ delete process.env.SETTINGS_JSON;
15
+ });
16
+
17
+ await test('it generates the API', async () => {
18
+ await fs.rm('./out/api_miyazaki_ebino', { recursive: true, force: true });
19
+ await main(['', '', './out/api_miyazaki_ebino']);
20
+ assert.ok(true);
21
+
22
+ const e = JSON.parse(await fs.readFile('./out/api_miyazaki_ebino/ja/宮崎県/えびの市.json', 'utf-8')) as MachiAzaApi;
23
+ const eData = e.data;
24
+ assert(eData.length > 100);
25
+ assert(eData.find((city) => city.machiaza_id === '0000110')?.koaza === '下村');
26
+ });
27
+ });
28
+
src/processes/02_make_machi_aza.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { getAndStreamCSVDataForId } from '../lib/ckan.js';
5
+ import { MachiAzaApi, SingleMachiAza } from '../data.js';
6
+ import { MachiAzaData, MachiAzaPosData } from '../lib/ckan_data/machi_aza.js';
7
+ import { mergeDataLeftJoin } from '../lib/ckan_data/index.js';
8
+ import { rawToMachiAza } from './02_machi_aza.js';
9
+ import { downloadAndExtractNlftpMlitFile, NlftpMlitDataRow } from '../lib/mlit_nlftp.js';
10
+ import { createMergedApiData, filterMlitDataByPrefCity } from '../lib/abr_mlit_merge_tools.js';
11
+
12
+ async function outputMachiAzaData(
13
+ outDir: string,
14
+ prefName: string,
15
+ cityName: string,
16
+ api: MachiAzaApi,
17
+ ): Promise<number> {
18
+ const outFile = path.join(outDir, 'ja', prefName, `${cityName}.json`);
19
+ await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
20
+ await fs.promises.writeFile(outFile, JSON.stringify(api));
21
+ console.log(`${prefName.padEnd(4, ' ')} ${cityName.padEnd(10, ' ')}: ${api.data.length.toString(10).padEnd(4, ' ')} 件の町字を出力した`);
22
+ return api.data.length;
23
+ }
24
+
25
+ async function main(argv: string[]) {
26
+ const updated = Math.floor(Date.now() / 1000);
27
+ const outDir = argv[2] || path.join(import.meta.dirname, '..', '..', 'out', 'api');
28
+ fs.mkdirSync(outDir, { recursive: true });
29
+
30
+
31
+ const mainStream = getAndStreamCSVDataForId<MachiAzaData>('ba-o1-000000_g2-000003');
32
+ const posStream = getAndStreamCSVDataForId<MachiAzaPosData>('ba000004');
33
+ const rawData = mergeDataLeftJoin(mainStream, posStream, ['lg_code', 'machiaza_id']);
34
+ // const rawData = mergeMachiAzaData(mainStream, posStream);
35
+
36
+ let lastLGCode: string | undefined = undefined;
37
+ let lastPrefName: string | undefined = undefined;
38
+ let lastCityName: string | undefined = undefined;
39
+ let lastMlitData: NlftpMlitDataRow[] | undefined = undefined;
40
+ let allCount = 0;
41
+ let apiData: SingleMachiAza[] = [];
42
+ for await (const raw of rawData) {
43
+ if (lastLGCode !== raw.lg_code && lastLGCode !== undefined) {
44
+ const filteredMlit = filterMlitDataByPrefCity(lastMlitData!, lastPrefName!, lastCityName!);
45
+ apiData = createMergedApiData(apiData, filteredMlit);
46
+ const api: MachiAzaApi = { meta: { updated }, data: apiData };
47
+ allCount += await outputMachiAzaData(outDir, lastPrefName!, lastCityName!, api);
48
+ apiData = [];
49
+ }
50
+ if (lastPrefName !== raw.pref) {
51
+ // 都道府県が変わったので、都道府県レベルの新しいデータを取得する
52
+ lastMlitData = await downloadAndExtractNlftpMlitFile(raw.lg_code.slice(0, 2));
53
+ }
54
+ if (lastLGCode !== raw.lg_code) {
55
+ lastLGCode = raw.lg_code;
56
+ lastPrefName = raw.pref;
57
+ lastCityName = `${raw.county}${raw.city}${raw.ward}`;
58
+ }
59
+
60
+ apiData.push(rawToMachiAza(raw));
61
+ }
62
+ if (lastLGCode) {
63
+ const filteredMlit = filterMlitDataByPrefCity(lastMlitData!, lastPrefName!, lastCityName!);
64
+ apiData = createMergedApiData(apiData, filteredMlit);
65
+ const api: MachiAzaApi = { meta: { updated }, data: apiData };
66
+ allCount += await outputMachiAzaData(outDir, lastPrefName!, lastCityName!, api);
67
+ }
68
+
69
+ console.log(`全国: ${allCount} 件の町字を出力した`);
70
+ }
71
+
72
+ export default main;
src/processes/03_make_rsdt.test.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert';
2
+ import test from 'node:test';
3
+
4
+ import fs from 'node:fs/promises';
5
+ import main from './03_make_rsdt.js';
6
+ import { getRangesFromCSV } from './10_refresh_csv_ranges.js';
7
+
8
+ await test.describe('with filter for 131059 (東京都文京区)', async () => {
9
+ test.before(() => {
10
+ process.env.SETTINGS_JSON = JSON.stringify({ lgCodes: ['131059'] });
11
+ });
12
+
13
+ test.after(() => {
14
+ delete process.env.SETTINGS_JSON;
15
+ });
16
+
17
+ await test('it generates the API', async () => {
18
+ await fs.rm('./out/api_tokyo_bunkyo', { recursive: true, force: true });
19
+ await main(['', '', './out/api_tokyo_bunkyo']);
20
+ assert.ok(true);
21
+
22
+ const headers = await getRangesFromCSV('./out/api_tokyo_bunkyo/ja/東京都/文京区-住居表示.txt');
23
+ assert(typeof headers !== 'undefined');
24
+ assert.equal(headers[0].name, '白山一丁目');
25
+ });
26
+ });
src/processes/03_make_rsdt.ts ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ckanPackageSearch, combineCSVParserIterators, CSVParserIterator, findResultByTypeAndArea, getAndParseCSVDataForId, getAndStreamCSVDataForId } from '../lib/ckan.js';
4
+ import { mergeRsdtdspRsdtData, RsdtdspRsdtData, RsdtdspRsdtPosData } from '../lib/ckan_data/rsdtdsp_rsdt.js';
5
+ import { machiAzaName, RsdtApi, SingleRsdt } from '../data.js';
6
+ import { projectABRData } from '../lib/proj.js';
7
+ import { MachiAzaData } from '../lib/ckan_data/machi_aza.js';
8
+ import { rawToMachiAza } from './02_machi_aza.js';
9
+ import { loadSettings } from '../lib/settings.js';
10
+
11
+ const HEADER_CHUNK_SIZE = 50_000;
12
+ // const HEADER_PBF_CHUNK_SIZE = 8_192;
13
+
14
+ function getOutPath(ma: MachiAzaData) {
15
+ return path.join(
16
+ ma.pref,
17
+ `${ma.county}${ma.city}${ma.ward}`,
18
+ );
19
+ }
20
+
21
+ type HeaderRow = {
22
+ name: string;
23
+ offset: number;
24
+ length: number;
25
+ }
26
+
27
+ function serializeApiDataTxt(apiData: RsdtApi): { headerIterations: number, headerData: HeaderRow[], data: Buffer } {
28
+ const outSections: Buffer[] = [];
29
+ for ( const { machiAza, rsdts } of apiData ) {
30
+ let outSection = `住居表示,${machiAzaName(machiAza)}\n` +
31
+ `blk_num,rsdt_num,rsdt_num2,lng,lat\n`;
32
+ for (const rsdt of rsdts) {
33
+ outSection += `${rsdt.blk_num || ''},${rsdt.rsdt_num},${rsdt.rsdt_num2 || ''},${rsdt.point?.[0] || ''},${rsdt.point?.[1] || ''}\n`;
34
+ }
35
+ outSections.push(Buffer.from(outSection, 'utf8'));
36
+ }
37
+
38
+ const createHeader = (iterations = 1) => {
39
+ let header = '';
40
+ const headerMaxSize = HEADER_CHUNK_SIZE * iterations;
41
+ let lastBytePos = headerMaxSize;
42
+ const headerData: HeaderRow[] = [];
43
+ for (const [index, section] of outSections.entries()) {
44
+ const ma = apiData[index].machiAza;
45
+
46
+ header += `${machiAzaName(ma)},${lastBytePos},${section.length}\n`;
47
+ headerData.push({
48
+ name: machiAzaName(ma),
49
+ offset: lastBytePos,
50
+ length: section.length,
51
+ });
52
+ lastBytePos += section.length;
53
+ }
54
+ const headerBuf = Buffer.from(header + '=END=\n', 'utf8');
55
+ if (headerBuf.length > headerMaxSize) {
56
+ return createHeader(iterations + 1);
57
+ } else {
58
+ const padding = Buffer.alloc(headerMaxSize - headerBuf.length);
59
+ padding.fill(0x20);
60
+ return {
61
+ iterations,
62
+ data: headerData,
63
+ buffer: Buffer.concat([headerBuf, padding])
64
+ };
65
+ }
66
+ };
67
+
68
+ const header = createHeader();
69
+ return {
70
+ headerIterations: header.iterations,
71
+ headerData: header.data,
72
+ data: Buffer.concat([header.buffer, ...outSections]),
73
+ };
74
+ }
75
+
76
+ // function _stringIfNotInteger(value: string | undefined) {
77
+ // if (!value) { return undefined; }
78
+ // return /^\d+$/.test(value) ? undefined : value;
79
+ // }
80
+
81
+ // function serializeApiDataPbf(apiData: RsdtApi): Buffer {
82
+ // let outSections: Buffer[] = [];
83
+ // for ( const { machiAza, rsdts } of apiData ) {
84
+ // const section: AddrData.Section = {
85
+ // kind: AddrData.Kind.RSDT,
86
+ // name: machiAzaName(machiAza),
87
+ // rsdtRows: [],
88
+ // chibanRows: [],
89
+ // }
90
+ // for (const rsdt of rsdts) {
91
+ // section.rsdtRows.push({
92
+ // blkNum: rsdt.blk_num ? parseInt(rsdt.blk_num, 10) : undefined,
93
+ // rsdtNum: parseInt(rsdt.rsdt_num, 10),
94
+ // rsdtNum2: rsdt.rsdt_num2 ? parseInt(rsdt.rsdt_num2, 10) : undefined,
95
+ // point: rsdt.point ? { lng: rsdt.point[0], lat: rsdt.point[1] } : undefined,
96
+ // blkNumStr: _stringIfNotInteger(rsdt.blk_num),
97
+ // rsdtNumStr: _stringIfNotInteger(rsdt.rsdt_num),
98
+ // rsdtNum2Str: _stringIfNotInteger(rsdt.rsdt_num2),
99
+ // });
100
+ // }
101
+ // const sectionBuf = Buffer.from(AddrData.Section.encode(section).finish());
102
+ // outSections.push(sectionBuf);
103
+ // }
104
+
105
+ // const createHeader = (iterations = 1) => {
106
+ // const header: AddrData.Header = {
107
+ // kind: AddrData.Kind.RSDT,
108
+ // rows: [],
109
+ // };
110
+ // const headerMaxSize = HEADER_PBF_CHUNK_SIZE * iterations;
111
+ // let lastBytePos = headerMaxSize;
112
+ // for (const [index, section] of outSections.entries()) {
113
+ // const ma = apiData[index].machiAza;
114
+
115
+ // header.rows.push({
116
+ // name: machiAzaName(ma),
117
+ // offset: lastBytePos,
118
+ // length: section.length,
119
+ // });
120
+ // lastBytePos += section.length;
121
+ // }
122
+ // const headerBuf = Buffer.from(AddrData.Header.encode(header).finish());
123
+ // if (headerBuf.length > headerMaxSize) {
124
+ // return createHeader(iterations + 1);
125
+ // } else {
126
+ // const padding = Buffer.alloc(headerMaxSize - headerBuf.length);
127
+ // padding.fill(0x00);
128
+ // return Buffer.concat([headerBuf, padding]);
129
+ // }
130
+ // };
131
+
132
+ // const header = createHeader();
133
+ // return Buffer.concat([header, ...outSections]);
134
+ // }
135
+
136
+ async function outputRsdtData(outDir: string, outFilename: string, apiData: RsdtApi) {
137
+ // const machiAzaJSON = path.join(outDir, 'ja', outFilename + '.json');
138
+ // fs.mkdirSync(path.dirname(machiAzaJSON), { recursive: true });
139
+ // fs.writeFileSync(outFileJSON, JSON.stringify(apiData));
140
+
141
+ const outFileTXT = path.join(outDir, 'ja', outFilename + '-住居表示.txt');
142
+ const txt = serializeApiDataTxt(apiData);
143
+ await fs.promises.mkdir(path.dirname(outFileTXT), { recursive: true });
144
+ await fs.promises.writeFile(outFileTXT, txt.data);
145
+
146
+ // const outFilePbf = path.join(outDir, 'ja', outFilename + '.pbf');
147
+ // fs.writeFileSync(outFilePbf, serializeApiDataPbf(apiData));
148
+
149
+ console.log(`${outFilename}-住居表示: ${apiData.length.toString(10).padEnd(4, ' ')} 件の町字を出力した`);
150
+ }
151
+
152
+ async function main(argv: string[]) {
153
+ const outDir = argv[2] || path.join(import.meta.dirname, '..', '..', 'out', 'api');
154
+ fs.mkdirSync(outDir, { recursive: true });
155
+
156
+ const machiAzaData = await getAndParseCSVDataForId<MachiAzaData>('ba-o1-000000_g2-000003'); // 市区町村 & 町字
157
+ const machiAzaDataByCode = new Map(machiAzaData.map((city) => [
158
+ `${city.lg_code}|${city.machiaza_id}`,
159
+ city
160
+ ]));
161
+
162
+ // 鹿児島県
163
+ // const mainStream = getAndStreamCSVDataForId<RsdtdspRsdtData>('ba-o1-460001_g2-000005');
164
+ // const posStream = getAndStreamCSVDataForId<RsdtdspRsdtPosData>('ba-o1-460001_g2-000008');
165
+
166
+ const hasFilter = (await loadSettings()).lgCodes.length > 0;
167
+
168
+ let mainStream: CSVParserIterator<RsdtdspRsdtData>;
169
+ let posStream: CSVParserIterator<RsdtdspRsdtPosData>;
170
+ if (!hasFilter) {
171
+ mainStream = getAndStreamCSVDataForId<RsdtdspRsdtData>('ba000003');
172
+ posStream = getAndStreamCSVDataForId<RsdtdspRsdtPosData>('ba000006');
173
+ } else {
174
+ // machiAzaData が既にフィルターされているので、そこからユニークな都道府県のみ抽出し、そのストリームのみ読み込むようにする
175
+ const prefs = new Set(machiAzaData.map((ma) => ma.pref));
176
+
177
+ const mainStreams: CSVParserIterator<RsdtdspRsdtData>[] = [];
178
+ const posStreams: CSVParserIterator<RsdtdspRsdtPosData>[] = [];
179
+ for (const pref of prefs) {
180
+ const mainSearchQuery = `${pref} 住居表示-住居マスター データセット`;
181
+ const mainResults = await ckanPackageSearch(mainSearchQuery);
182
+ const main = findResultByTypeAndArea(mainResults, '住居表示-住居マスター(都道府県)', pref);
183
+ if (!main) {
184
+ throw new Error(`「${pref}」の住居表示-住居マスター データセットが見つかりませんでした`);
185
+ }
186
+ mainStreams.push(getAndStreamCSVDataForId<RsdtdspRsdtData>(main.id));
187
+
188
+ const posSearchQuery = `${pref} 住居表示-住居マスター位置参照拡張 データセット`;
189
+ const posResults = await ckanPackageSearch(posSearchQuery);
190
+ const pos = findResultByTypeAndArea(posResults, '住居表示-住居マスター位置参照拡張(都道府県)', pref);
191
+ if (!pos) {
192
+ throw new Error(`「${pref}」の住居表示-住居マスター位置参照拡張 データセットが見つかりませんでした`);
193
+ }
194
+ posStreams.push(getAndStreamCSVDataForId<RsdtdspRsdtPosData>(pos.id));
195
+ }
196
+ mainStream = combineCSVParserIterators(...mainStreams);
197
+ posStream = combineCSVParserIterators(...posStreams);
198
+ }
199
+ const rawData = mergeRsdtdspRsdtData(mainStream, posStream);
200
+
201
+ let lastOutPath: string | undefined = undefined;
202
+
203
+ let apiData: RsdtApi = [];
204
+ let currentRsdtList: SingleRsdt[] = [];
205
+ let currentMachiAza: MachiAzaData | undefined = undefined;
206
+ for await (const raw of rawData) {
207
+ const ma = machiAzaDataByCode.get(`${raw.lg_code}|${raw.machiaza_id}`);
208
+ if (!ma) {
209
+ continue;
210
+ }
211
+ const thisOutPath = getOutPath(ma);
212
+ if (currentMachiAza && (currentMachiAza.machiaza_id !== ma.machiaza_id || currentMachiAza.lg_code !== ma.lg_code)) {
213
+ if (currentRsdtList.length > 0) {
214
+ apiData.push({
215
+ machiAza: rawToMachiAza(currentMachiAza),
216
+ rsdts: currentRsdtList,
217
+ });
218
+ }
219
+ currentMachiAza = ma;
220
+ currentRsdtList = [];
221
+ }
222
+ if (lastOutPath !== thisOutPath && lastOutPath !== undefined) {
223
+ await outputRsdtData(outDir, lastOutPath, apiData);
224
+ apiData = [];
225
+ }
226
+ if (lastOutPath !== thisOutPath) {
227
+ lastOutPath = thisOutPath;
228
+ }
229
+ if (!currentMachiAza) {
230
+ currentMachiAza = ma;
231
+ }
232
+
233
+ currentRsdtList.push({
234
+ blk_num: raw.blk_num === '' ? undefined : raw.blk_num,
235
+ rsdt_num: raw.rsdt_num,
236
+ rsdt_num2: raw.rsdt_num2 === '' ? undefined : raw.rsdt_num2,
237
+ point: 'rep_srid' in raw ? projectABRData(raw) : undefined,
238
+ });
239
+ }
240
+ if (currentMachiAza && currentRsdtList.length > 0) {
241
+ apiData.push({
242
+ machiAza: rawToMachiAza(currentMachiAza),
243
+ rsdts: currentRsdtList,
244
+ });
245
+ }
246
+ if (lastOutPath) {
247
+ await outputRsdtData(outDir, lastOutPath, apiData);
248
+ }
249
+ }
250
+
251
+ export default main;