@jpzip/jpzip 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +265 -0
- package/README.md +231 -49
- package/package.json +1 -1
package/README.ja.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
## jpzip-js
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
4
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/jpzip/js/actions/workflows/test.yml)
|
|
7
|
+
|
|
8
|
+
> **jpzip** の TypeScript / JavaScript SDK — 無料・無制限の日本郵便番号 API。
|
|
9
|
+
> 日本郵便の `KEN_ALL.csv` / `KEN_ALL_ROME.csv` を JSON 正規化し CDN 配信。
|
|
10
|
+
|
|
11
|
+
[English](./README.md) | **日本語**
|
|
12
|
+
|
|
13
|
+
`@jpzip/jpzip` は `jpzip.nadai.dev` から日本の郵便番号 120,677 件を引く TypeScript SDK です。
|
|
14
|
+
登録不要、レート制限なし、API キー不要。
|
|
15
|
+
|
|
16
|
+
- 🇯🇵 **全件収録** — 漢字・カナ・ローマ字・自治体コード(JIS X 0401 / 総務省地方公共団体コード)
|
|
17
|
+
- ⚡️ **高速** — L1 LRU + 任意の L2 永続キャッシュ。`preload` でネットワーク往復なしのルックアップが可能
|
|
18
|
+
- 🛡️ **堅牢** — 5xx / ネットワーク失敗時は指数バックオフで最大 3 回リトライ
|
|
19
|
+
- 🪶 **ランタイム依存ゼロ** — プラットフォーム標準の `fetch` のみ使用
|
|
20
|
+
- 🧰 **型安全** — TypeScript ファーストクラス、ESM + CJS デュアルビルド
|
|
21
|
+
- 🆓 **永久無料** — Cloudflare Pages 無料枠で運用(課金軸が存在しない)
|
|
22
|
+
- 🔌 **同一 API** — [全 jpzip SDK](#他言語版) で API が揃う
|
|
23
|
+
|
|
24
|
+
## 必要環境
|
|
25
|
+
|
|
26
|
+
Node.js 18+(または `fetch` が標準搭載されたランタイム — Bun、Deno、モダンブラウザ、Cloudflare Workers、Vercel Edge)。
|
|
27
|
+
|
|
28
|
+
## インストール
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @jpzip/jpzip
|
|
32
|
+
# または
|
|
33
|
+
pnpm add @jpzip/jpzip
|
|
34
|
+
# または
|
|
35
|
+
yarn add @jpzip/jpzip
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## クイックスタート
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { lookup } from '@jpzip/jpzip';
|
|
42
|
+
|
|
43
|
+
const entry = await lookup('2310017');
|
|
44
|
+
if (entry === null) {
|
|
45
|
+
console.log('見つかりません');
|
|
46
|
+
} else {
|
|
47
|
+
console.log(entry.prefecture, entry.city, entry.towns[0].town);
|
|
48
|
+
// 出力: 神奈川県 横浜市中区 港町
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
ローマ字・自治体コードも同じエントリに含まれます:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
console.log(entry.prefecture_roma, entry.city_roma, entry.towns[0].roma);
|
|
56
|
+
// 出力: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
|
|
57
|
+
|
|
58
|
+
console.log(entry.prefecture_code, entry.city_code);
|
|
59
|
+
// 出力: 14 14104
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## ユースケース
|
|
63
|
+
|
|
64
|
+
### 郵便番号ルックアップ HTTP エンドポイント (Hono)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { Hono } from 'hono';
|
|
68
|
+
import { lookup } from '@jpzip/jpzip';
|
|
69
|
+
|
|
70
|
+
const app = new Hono();
|
|
71
|
+
|
|
72
|
+
app.get('/api/zipcode/:code', async (c) => {
|
|
73
|
+
const entry = await lookup(c.req.param('code'));
|
|
74
|
+
if (entry === null) return c.notFound();
|
|
75
|
+
return c.json(entry);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default app;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Node / Bun / Cloudflare Workers / Vercel Edge でそのまま動作します。
|
|
82
|
+
|
|
83
|
+
### 郵便番号ルックアップ HTTP エンドポイント (Express)
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import express from 'express';
|
|
87
|
+
import { lookup } from '@jpzip/jpzip';
|
|
88
|
+
|
|
89
|
+
const app = express();
|
|
90
|
+
|
|
91
|
+
app.get('/api/zipcode/:code', async (req, res, next) => {
|
|
92
|
+
try {
|
|
93
|
+
const entry = await lookup(req.params.code);
|
|
94
|
+
if (entry === null) return res.status(404).end();
|
|
95
|
+
res.json(entry);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
next(err);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.listen(3000);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### CSV のバッチ検証
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { lookupAll } from '@jpzip/jpzip';
|
|
108
|
+
|
|
109
|
+
const all = await lookupAll(); // 全件をメモリに展開(JSON 約 37 MiB)
|
|
110
|
+
for (const zip of csvZipcodes) {
|
|
111
|
+
if (!(zip in all)) {
|
|
112
|
+
console.warn(`不正な郵便番号: ${zip}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### キャッシュからの提供(任意の L2 バックエンド)
|
|
118
|
+
|
|
119
|
+
データは 948 個の 3 桁 prefix バケットに分割されています。デフォルト L1 (100 件) は
|
|
120
|
+
ホットなバケットを保持しますが、全件を常駐させるには L2 を併用するか
|
|
121
|
+
`memoryCacheSize` を 948 超に設定してください。
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { JpzipClient, type PersistentCache } from '@jpzip/jpzip';
|
|
125
|
+
import { readFile, writeFile, unlink, rm } from 'node:fs/promises';
|
|
126
|
+
import { join } from 'node:path';
|
|
127
|
+
import { createHash } from 'node:crypto';
|
|
128
|
+
|
|
129
|
+
const dir = '.jpzip-cache';
|
|
130
|
+
const path = (key: string) => join(dir, createHash('sha1').update(key).digest('hex'));
|
|
131
|
+
|
|
132
|
+
const fileCache: PersistentCache = {
|
|
133
|
+
async get(key) {
|
|
134
|
+
try {
|
|
135
|
+
return await readFile(path(key));
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async set(key, value) {
|
|
141
|
+
await writeFile(path(key), value);
|
|
142
|
+
},
|
|
143
|
+
async delete(key) {
|
|
144
|
+
await unlink(path(key)).catch(() => {});
|
|
145
|
+
},
|
|
146
|
+
async clear() {
|
|
147
|
+
await rm(dir, { recursive: true, force: true });
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const client = new JpzipClient({
|
|
152
|
+
memoryCacheSize: 1024,
|
|
153
|
+
cache: fileCache,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await client.preload({ scope: 'all' });
|
|
157
|
+
// 以降の lookup は L1/L2 で完結し、ネットワークにアクセスしない
|
|
158
|
+
const entry = await client.lookup('2310017');
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## API リファレンス
|
|
162
|
+
|
|
163
|
+
### 関数(モジュールレベル、内部の default `JpzipClient` シングルトンを共有)
|
|
164
|
+
|
|
165
|
+
| 関数 | 説明 |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `lookup(zipcode)` | 7 桁の郵便番号で 1 件引く。見つからない / 不正な入力は `null`(不正入力時はネットワーク不使用)。 |
|
|
168
|
+
| `lookupGroup(prefix)` | 1〜3 桁の prefix で引く。1 桁は `/g/{d}.json` を 1 回、3 桁は `/p/{ddd}.json` を 1 回、2 桁は 10 並列 fetch して結合。数字以外を含む入力では throw。 |
|
|
169
|
+
| `lookupAll()` | `/g/0..9.json` を並列取得して全件(120k 件、約 37 MiB)を返す。 |
|
|
170
|
+
| `getMeta()` | データバージョン・生成日時・都道府県別件数・spec version。client の `refresh()` を呼ぶまで結果をキャッシュ。 |
|
|
171
|
+
| `preload({ scope })` | `'all'` または 1〜3 桁の prefix で L1(L2 設定時は L2 も)を温める。 |
|
|
172
|
+
| `isValidZipcode(zip)` | 純粋な書式チェック(`^\d{7}$`)。ネットワーク不使用。 |
|
|
173
|
+
| `configure(options)` | シングルトンを差し替え、以降の関数呼び出しに新オプションを適用。 |
|
|
174
|
+
|
|
175
|
+
### `JpzipClient`(高度な用途)
|
|
176
|
+
|
|
177
|
+
L2 キャッシュ、`fetch` 差し替え、配信元変更、複数の独立キャッシュが必要な場合にインスタンスを直接生成します:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { JpzipClient } from '@jpzip/jpzip';
|
|
181
|
+
|
|
182
|
+
const client = new JpzipClient({
|
|
183
|
+
baseUrl: 'https://jpzip.nadai.dev',
|
|
184
|
+
fetch: globalThis.fetch, // 任意で差し替え
|
|
185
|
+
memoryCacheSize: 200, // L1 容量(prefix バケット数)、デフォルト 100
|
|
186
|
+
cache: myPersistentCache, // L2(任意)
|
|
187
|
+
onSpecMismatch: ({ expected, received }) => {
|
|
188
|
+
console.warn(`jpzip spec 不一致: SDK=${expected} server=${received}`);
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`JpzipClient` は `lookup` / `lookupGroup` / `lookupAll` / `getMeta` / `preload` に加えて:
|
|
194
|
+
|
|
195
|
+
| メソッド | 説明 |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `client.refresh()` | L1(L2 設定時は L2 も)を消し、キャッシュ済み meta を破棄。 |
|
|
198
|
+
|
|
199
|
+
`getMeta()` が `/meta.json` の `version` 変更を検知すると L1/L2 が自動クリアされます。
|
|
200
|
+
データ切り替えに追従するには `getMeta()` を定期的に呼んでください。
|
|
201
|
+
|
|
202
|
+
### エラー
|
|
203
|
+
|
|
204
|
+
- `lookup()` は「見つからない」「書式不正」いずれの場合も throw せず `null` を返します。
|
|
205
|
+
- `lookupGroup(prefix)` は `prefix` が `/^\d{1,3}$/` に一致しない場合 `Error` を throw。
|
|
206
|
+
- ネットワーク失敗と 5xx は最大 3 回試行(初回 + リトライ 2 回)、指数バックオフのスリープは 400ms / 800ms。404 以外の 4xx は即時 throw、404 は `null` 返却。`AbortError` はリトライせず再 throw。
|
|
207
|
+
|
|
208
|
+
### `PersistentCache` インターフェース
|
|
209
|
+
|
|
210
|
+
任意の L2 バックエンド(ファイル / IndexedDB / Redis / Cloudflare KV など)を渡せます:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
export interface PersistentCache {
|
|
214
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
215
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
216
|
+
delete(key: string): Promise<void>;
|
|
217
|
+
clear(): Promise<void>;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
キーは prefix バケットの完全 URL(例: `https://jpzip.nadai.dev/p/231.json`)、値は生 JSON バイト列。
|
|
222
|
+
|
|
223
|
+
### エクスポートされる型
|
|
224
|
+
|
|
225
|
+
`ZipcodeEntry` / `Town` / `Meta` / `ZipcodeDict` / `Endpoints` / `JpzipClientOptions` / `PersistentCache` — すべて `@jpzip/jpzip` から import 可能。
|
|
226
|
+
|
|
227
|
+
## なぜ jpzip-js か
|
|
228
|
+
|
|
229
|
+
| | **jpzip-js** | [jpostcode][jpostcode] | [jposta][jposta] | [@ken-all/kenall][kenall] |
|
|
230
|
+
|---|---|---|---|---|
|
|
231
|
+
| ローマ字(`Yokohama Shi`) | ✅ | ❌ | ❌ | ⚠️ オプション |
|
|
232
|
+
| 自治体コード(JIS / 総務省) | ✅ | ❌ | ⚠️ `prefNum` のみ | ✅ |
|
|
233
|
+
| カナ | ✅ | ✅ | ❌ | ✅ |
|
|
234
|
+
| ビルドに数 MB のデータを同梱しない | ✅ CDN fetch | ⚠️ npm 版は同梱 | ❌ JSON 同梱 | ✅ |
|
|
235
|
+
| `npm install` 不要で月次更新 | ✅ CDN で自動 | ❌ 月次再公開 | ❌ 手動 | ✅ |
|
|
236
|
+
| API キー不要 | ✅ | ✅ | ✅ | ❌ 必須 |
|
|
237
|
+
| レート制限なし | ✅ | ✅ | ✅ | ⚠️ プラン依存 |
|
|
238
|
+
| L1 + 差し替え可能な L2 | ✅ | ❌ | ❌ | ❌ |
|
|
239
|
+
| ランタイム依存ゼロ | ✅ | ✅ | ✅ | ❌ (`zod`) |
|
|
240
|
+
| ESM + CJS + TypeScript 型 | ✅ | ✅ | ✅ | ✅ |
|
|
241
|
+
|
|
242
|
+
[jpostcode]: https://www.npmjs.com/package/jpostcode
|
|
243
|
+
[jposta]: https://www.npmjs.com/package/jposta
|
|
244
|
+
[kenall]: https://www.npmjs.com/package/@ken-all/kenall
|
|
245
|
+
|
|
246
|
+
## 他言語版
|
|
247
|
+
|
|
248
|
+
全 SDK で同一の API を提供しています:
|
|
249
|
+
|
|
250
|
+
[Go](https://github.com/jpzip/go) · [Python](https://github.com/jpzip/python) · [Rust](https://github.com/jpzip/rust) · [Ruby](https://github.com/jpzip/ruby) · [PHP](https://github.com/jpzip/php) · [Swift](https://github.com/jpzip/swift) · [Dart](https://github.com/jpzip/dart)
|
|
251
|
+
|
|
252
|
+
## 関連リソース
|
|
253
|
+
|
|
254
|
+
- **Web サイト** — https://jpzip.nadai.dev
|
|
255
|
+
- **プロトコル仕様** — [jpzip/spec](https://github.com/jpzip/spec)
|
|
256
|
+
- **データ ETL** — [jpzip/data](https://github.com/jpzip/data)
|
|
257
|
+
- **MCP サーバー** — [jpzip/mcp](https://github.com/jpzip/mcp) — Claude / ChatGPT / Cursor から jpzip を呼ぶ
|
|
258
|
+
|
|
259
|
+
## キーワード
|
|
260
|
+
|
|
261
|
+
日本郵便番号, 郵便番号, KEN_ALL, KEN_ALL_ROME, 住所バリデーション, 住所検索, japanese postal code, japan zipcode, postal code lookup typescript, typescript japanese address, node.js zipcode, hono 郵便番号, express 郵便番号, cloudflare workers 住所, JIS X 0401, 総務省地方公共団体コード
|
|
262
|
+
|
|
263
|
+
## ライセンス
|
|
264
|
+
|
|
265
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,88 +1,270 @@
|
|
|
1
|
-
|
|
1
|
+
## jpzip-js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
4
|
+
[](https://www.npmjs.com/package/@jpzip/jpzip)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/jpzip/js/actions/workflows/test.yml)
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- データ ETL: [`jpzip/data`](https://github.com/jpzip/data)
|
|
8
|
+
> TypeScript / JavaScript SDK for **jpzip** — a free, unlimited Japanese postal code (郵便番号) API.
|
|
9
|
+
> 日本の全郵便番号 120,677 件を CDN 配信 JSON から引く TypeScript SDK。
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
**English** | [日本語](./README.ja.md)
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
`@jpzip/jpzip` looks up Japanese postal codes (郵便番号) from `jpzip.nadai.dev`,
|
|
14
|
+
a CDN-hosted dataset built from Japan Post's `KEN_ALL.csv` and `KEN_ALL_ROME.csv`
|
|
15
|
+
normalized to JSON. No registration, no rate limits, no API key.
|
|
16
|
+
|
|
17
|
+
- 🇯🇵 **Complete dataset** — 120,677 entries with kanji, kana, romaji, and government codes (JIS X 0401 / 総務省地方公共団体コード)
|
|
18
|
+
- ⚡️ **Fast** — L1 LRU + optional L2 persistent cache; `preload` to serve lookups without per-request network round-trips
|
|
19
|
+
- 🛡️ **Resilient** — up to 3 attempts with exponential backoff on 5xx / network failures
|
|
20
|
+
- 🪶 **Zero runtime deps** — uses the platform `fetch` only
|
|
21
|
+
- 🧰 **Typed end to end** — first-class TypeScript, ESM + CJS dual build
|
|
22
|
+
- 🆓 **Free forever** — backed by Cloudflare Pages' free tier (no billing axis exists)
|
|
23
|
+
- 🔌 **Drop-in** — same API surface across [every jpzip SDK](#other-languages)
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
Node.js 18+ (or any runtime with a global `fetch` — Bun, Deno, modern browsers, Cloudflare Workers, Vercel Edge).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
12
32
|
npm install @jpzip/jpzip
|
|
13
33
|
# or
|
|
14
34
|
pnpm add @jpzip/jpzip
|
|
35
|
+
# or
|
|
36
|
+
yarn add @jpzip/jpzip
|
|
15
37
|
```
|
|
16
38
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
### 関数 API
|
|
39
|
+
## Quick Start
|
|
20
40
|
|
|
21
41
|
```ts
|
|
22
|
-
import { lookup
|
|
42
|
+
import { lookup } from '@jpzip/jpzip';
|
|
23
43
|
|
|
24
44
|
const entry = await lookup('2310017');
|
|
25
|
-
|
|
45
|
+
if (entry === null) {
|
|
46
|
+
console.log('not found');
|
|
47
|
+
} else {
|
|
48
|
+
console.log(entry.prefecture, entry.city, entry.towns[0].town);
|
|
49
|
+
// Output: 神奈川県 横浜市中区 港町
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Romaji and government codes are included on the same entry:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
console.log(entry.prefecture_roma, entry.city_roma, entry.towns[0].roma);
|
|
57
|
+
// Output: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
|
|
58
|
+
|
|
59
|
+
console.log(entry.prefecture_code, entry.city_code);
|
|
60
|
+
// Output: 14 14104
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Use Cases
|
|
64
|
+
|
|
65
|
+
### Zipcode lookup HTTP endpoint (Hono)
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { Hono } from 'hono';
|
|
69
|
+
import { lookup } from '@jpzip/jpzip';
|
|
70
|
+
|
|
71
|
+
const app = new Hono();
|
|
72
|
+
|
|
73
|
+
app.get('/api/zipcode/:code', async (c) => {
|
|
74
|
+
const entry = await lookup(c.req.param('code'));
|
|
75
|
+
if (entry === null) return c.notFound();
|
|
76
|
+
return c.json(entry);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export default app;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Works unchanged on Node, Bun, Cloudflare Workers, and Vercel Edge.
|
|
83
|
+
|
|
84
|
+
### Zipcode lookup HTTP endpoint (Express)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import express from 'express';
|
|
88
|
+
import { lookup } from '@jpzip/jpzip';
|
|
89
|
+
|
|
90
|
+
const app = express();
|
|
91
|
+
|
|
92
|
+
app.get('/api/zipcode/:code', async (req, res, next) => {
|
|
93
|
+
try {
|
|
94
|
+
const entry = await lookup(req.params.code);
|
|
95
|
+
if (entry === null) return res.status(404).end();
|
|
96
|
+
res.json(entry);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
next(err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.listen(3000);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Batch validation
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { lookupAll } from '@jpzip/jpzip';
|
|
109
|
+
|
|
110
|
+
const all = await lookupAll(); // entire dataset in memory (~37 MiB JSON)
|
|
111
|
+
for (const zip of csvZipcodes) {
|
|
112
|
+
if (!(zip in all)) {
|
|
113
|
+
console.warn(`invalid zipcode: ${zip}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Serve lookups from cache (BYO L2 backend)
|
|
119
|
+
|
|
120
|
+
The dataset is partitioned into 948 three-digit prefix buckets. The default
|
|
121
|
+
L1 (100 entries) keeps the hottest buckets; to cache the whole dataset, pair
|
|
122
|
+
`preload({ scope: 'all' })` with an L2 cache or raise `memoryCacheSize` above 948.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { JpzipClient, type PersistentCache } from '@jpzip/jpzip';
|
|
126
|
+
import { readFile, writeFile, unlink, rm } from 'node:fs/promises';
|
|
127
|
+
import { join } from 'node:path';
|
|
128
|
+
import { createHash } from 'node:crypto';
|
|
129
|
+
|
|
130
|
+
const dir = '.jpzip-cache';
|
|
131
|
+
const path = (key: string) => join(dir, createHash('sha1').update(key).digest('hex'));
|
|
132
|
+
|
|
133
|
+
const fileCache: PersistentCache = {
|
|
134
|
+
async get(key) {
|
|
135
|
+
try {
|
|
136
|
+
return await readFile(path(key));
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async set(key, value) {
|
|
142
|
+
await writeFile(path(key), value);
|
|
143
|
+
},
|
|
144
|
+
async delete(key) {
|
|
145
|
+
await unlink(path(key)).catch(() => {});
|
|
146
|
+
},
|
|
147
|
+
async clear() {
|
|
148
|
+
await rm(dir, { recursive: true, force: true });
|
|
149
|
+
},
|
|
150
|
+
};
|
|
26
151
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
152
|
+
const client = new JpzipClient({
|
|
153
|
+
memoryCacheSize: 1024,
|
|
154
|
+
cache: fileCache,
|
|
155
|
+
});
|
|
30
156
|
|
|
31
|
-
|
|
32
|
-
|
|
157
|
+
await client.preload({ scope: 'all' });
|
|
158
|
+
// Subsequent lookups are served from L1/L2 without hitting the network.
|
|
159
|
+
const entry = await client.lookup('2310017');
|
|
33
160
|
```
|
|
34
161
|
|
|
35
|
-
|
|
162
|
+
## API Reference
|
|
163
|
+
|
|
164
|
+
### Functions (module-level, share a default `JpzipClient` singleton)
|
|
165
|
+
|
|
166
|
+
| Function | Description |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `lookup(zipcode)` | Look up a single 7-digit zipcode. Returns `null` for not-found or malformed input (no network call for malformed input). |
|
|
169
|
+
| `lookupGroup(prefix)` | Look up by 1-, 2-, or 3-digit prefix. 1-digit fetches `/g/{d}.json`; 3-digit fetches `/p/{ddd}.json`; 2-digit fans out into 10 parallel 3-digit fetches and merges. Throws on non-digit input. |
|
|
170
|
+
| `lookupAll()` | Fetch entire dataset (120k entries, ~37 MiB) in parallel across `/g/0..9.json`. |
|
|
171
|
+
| `getMeta()` | Dataset version, generated-at, per-prefecture counts, spec version. Result is cached until `refresh()` is called on the client. |
|
|
172
|
+
| `preload({ scope })` | Warm L1 (and L2 when configured) for `'all'` or a specific 1-3 digit prefix. |
|
|
173
|
+
| `isValidZipcode(zip)` | Pure syntax check (`^\d{7}$`) — no network. |
|
|
174
|
+
| `configure(options)` | Replace the singleton with a new `JpzipClient` configured with `options`. |
|
|
175
|
+
|
|
176
|
+
### `JpzipClient` (advanced)
|
|
177
|
+
|
|
178
|
+
Construct an instance for L2 caching, custom `fetch`, alternate base URL, or
|
|
179
|
+
multiple isolated caches:
|
|
36
180
|
|
|
37
181
|
```ts
|
|
38
182
|
import { JpzipClient } from '@jpzip/jpzip';
|
|
39
183
|
|
|
40
184
|
const client = new JpzipClient({
|
|
41
185
|
baseUrl: 'https://jpzip.nadai.dev',
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
cache:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
async delete(k) { /* ... */ },
|
|
48
|
-
async clear() { /* ... */ },
|
|
186
|
+
fetch: globalThis.fetch, // optional override
|
|
187
|
+
memoryCacheSize: 200, // L1 capacity in prefix buckets, default 100
|
|
188
|
+
cache: myPersistentCache, // optional L2
|
|
189
|
+
onSpecMismatch: ({ expected, received }) => {
|
|
190
|
+
console.warn(`jpzip spec mismatch: SDK=${expected} server=${received}`);
|
|
49
191
|
},
|
|
50
192
|
});
|
|
193
|
+
```
|
|
51
194
|
|
|
52
|
-
|
|
53
|
-
|
|
195
|
+
`JpzipClient` exposes `lookup` / `lookupGroup` / `lookupAll` / `getMeta` / `preload` plus:
|
|
196
|
+
|
|
197
|
+
| Method | Description |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `client.refresh()` | Wipe L1 (and L2 when configured) and forget the cached meta. |
|
|
200
|
+
|
|
201
|
+
When `getMeta()` observes that `/meta.json`'s `version` has changed since the last
|
|
202
|
+
successful fetch, L1 and L2 are cleared automatically — call `getMeta()` periodically
|
|
203
|
+
to pick up dataset rollovers.
|
|
204
|
+
|
|
205
|
+
### Errors
|
|
206
|
+
|
|
207
|
+
- `lookup()` returns `null` rather than throwing for both "not found" and malformed input.
|
|
208
|
+
- `lookupGroup(prefix)` throws `Error` if `prefix` doesn't match `/^\d{1,3}$/`.
|
|
209
|
+
- Transient network failures and 5xx responses are retried up to 3 attempts (initial + 2 retries) with exponential backoff sleeps of 400ms and 800ms. 4xx responses other than 404 throw immediately; 404 yields `null`. `AbortError` propagates without retry.
|
|
210
|
+
|
|
211
|
+
### `PersistentCache` interface
|
|
212
|
+
|
|
213
|
+
Bring your own L2 backend (file system, IndexedDB, Redis, Cloudflare KV, etc.):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
export interface PersistentCache {
|
|
217
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
218
|
+
set(key: string, value: Uint8Array): Promise<void>;
|
|
219
|
+
delete(key: string): Promise<void>;
|
|
220
|
+
clear(): Promise<void>;
|
|
221
|
+
}
|
|
54
222
|
```
|
|
55
223
|
|
|
56
|
-
|
|
224
|
+
Keys are the full bucket URLs (e.g. `https://jpzip.nadai.dev/p/231.json`); values
|
|
225
|
+
are raw JSON bytes.
|
|
57
226
|
|
|
58
|
-
|
|
59
|
-
|---|---|---|
|
|
60
|
-
| **L1 メモリ LRU** | 同一プロセスの重複 fetch 抑制 | 常時 ON、prefix 100 件保持 |
|
|
61
|
-
| **L2 永続キャッシュ** | 起動またぎ / preload 結果保持 | OFF、`cache` オプションで有効化 |
|
|
62
|
-
| **L3 HTTP** | ブラウザ / OS のキャッシュ | SDK 制御外 |
|
|
227
|
+
### Exported types
|
|
63
228
|
|
|
64
|
-
|
|
229
|
+
`ZipcodeEntry`, `Town`, `Meta`, `ZipcodeDict`, `Endpoints`, `JpzipClientOptions`,
|
|
230
|
+
`PersistentCache` — all importable from `@jpzip/jpzip`.
|
|
65
231
|
|
|
66
|
-
##
|
|
232
|
+
## Why jpzip-js?
|
|
67
233
|
|
|
68
|
-
|
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
| `
|
|
75
|
-
|
|
|
76
|
-
|
|
|
234
|
+
| | **jpzip-js** | [jpostcode][jpostcode] | [jposta][jposta] | [@ken-all/kenall][kenall] |
|
|
235
|
+
|---|---|---|---|---|
|
|
236
|
+
| Romaji (`Yokohama Shi`) | ✅ | ❌ | ❌ | ⚠️ Optional field |
|
|
237
|
+
| Government codes (JIS / 総務省) | ✅ | ❌ | ⚠️ `prefNum` only | ✅ |
|
|
238
|
+
| Kana | ✅ | ✅ | ❌ | ✅ |
|
|
239
|
+
| No bundled multi-MB data in your build | ✅ CDN fetch | ⚠️ npm version embeds data | ❌ Embedded JSON | ✅ |
|
|
240
|
+
| Monthly updates without re-`npm install` | ✅ Auto via CDN | ❌ Republished monthly | ❌ Manual | ✅ |
|
|
241
|
+
| No API key | ✅ | ✅ | ✅ | ❌ Required |
|
|
242
|
+
| Rate-limit-free | ✅ | ✅ | ✅ | ⚠️ Plan-based quota |
|
|
243
|
+
| L1 + pluggable L2 cache | ✅ | ❌ | ❌ | ❌ |
|
|
244
|
+
| Zero runtime deps | ✅ | ✅ | ✅ | ❌ (`zod`) |
|
|
245
|
+
| ESM + CJS + TypeScript types | ✅ | ✅ | ✅ | ✅ |
|
|
246
|
+
|
|
247
|
+
[jpostcode]: https://www.npmjs.com/package/jpostcode
|
|
248
|
+
[jposta]: https://www.npmjs.com/package/jposta
|
|
249
|
+
[kenall]: https://www.npmjs.com/package/@ken-all/kenall
|
|
250
|
+
|
|
251
|
+
## Other Languages
|
|
252
|
+
|
|
253
|
+
Same API surface across all SDKs:
|
|
254
|
+
|
|
255
|
+
[Go](https://github.com/jpzip/go) · [Python](https://github.com/jpzip/python) · [Rust](https://github.com/jpzip/rust) · [Ruby](https://github.com/jpzip/ruby) · [PHP](https://github.com/jpzip/php) · [Swift](https://github.com/jpzip/swift) · [Dart](https://github.com/jpzip/dart)
|
|
77
256
|
|
|
78
|
-
##
|
|
257
|
+
## Resources
|
|
79
258
|
|
|
80
|
-
|
|
259
|
+
- **Website** — https://jpzip.nadai.dev
|
|
260
|
+
- **Protocol spec** — [jpzip/spec](https://github.com/jpzip/spec)
|
|
261
|
+
- **Data ETL** — [jpzip/data](https://github.com/jpzip/data)
|
|
262
|
+
- **MCP server** — [jpzip/mcp](https://github.com/jpzip/mcp) — use jpzip from Claude / ChatGPT / Cursor
|
|
81
263
|
|
|
82
|
-
##
|
|
264
|
+
## Keywords
|
|
83
265
|
|
|
84
|
-
|
|
266
|
+
japanese postal code, japan zipcode, 郵便番号, KEN_ALL, KEN_ALL_ROME, address validation, postal code lookup typescript, typescript japanese address, node.js zipcode, hono postal code, express zipcode, cloudflare workers japan address, JIS X 0401, 総務省地方公共団体コード
|
|
85
267
|
|
|
86
|
-
##
|
|
268
|
+
## License
|
|
87
269
|
|
|
88
270
|
[MIT](./LICENSE)
|