はじめに
jpzip は静的 JSON ファイル群を https://jpzip.nadai.dev から配信するデータセットです。各言語の SDK は
このデータを取得し、メモリ / 永続キャッシュを管理し、オフラインでも動かすための薄いラッパーを提供します。
インストール
# JavaScript / TypeScript (Node / browser / Bun / Deno)
npm i @jpzip/jpzip
# Go (1.21+)
go get github.com/jpzip/go
# Python (3.10+)
pip install jpzip
# Rust (1.75+)
cargo add jpzip
# Ruby (3.2+)
gem install jpzip
# Dart (3.0+) / Flutter
dart pub add jpzip
# PHP (8.2+)
composer require jpzip/jpzip
# Swift (5.9+) — SwiftPM Package.swift
.package(url: "https://github.com/jpzip/swift", from: "0.1.0") 60 秒クイックスタート
最小の使用例 — シングルトンが自動で初期化されるので、いきなり呼べます。下の SDK セクションで言語タブを切り替えてください。
SDK
言語タブで切り替えてください。URL の hash (例: #go-client) で直接該当言語にも飛べます。
import { lookup } from "@jpzip/jpzip";
const e = await lookup("2310017");
if (e) console.log(e.prefecture, e.city, e.towns[0].town);
// 神奈川県 横浜市中区 本町
npm パッケージ名 @jpzip/jpzip。ESM / CJS 両対応、完全型付き、Node 18+ / 主要ブラウザ / Bun / Deno /
Cloudflare Workers で動作します。
関数 API (シングルトン)
1 つの app に 1 つのクライアントで足りる場合は関数 API が最も簡潔です。内部で遅延初期化された JpzipClient を共有し、L1 メモリキャッシュが効きます。
import {
lookup, // (zip: string) => Promise<ZipcodeEntry | null>
lookupGroup, // (prefix: 1-3桁) => Promise<ZipcodeDict>
lookupAll, // () => Promise<ZipcodeDict> (内部で /g/0..9 並列)
preload, // ({ scope: 'all' | '0' | '23' | '231' }) => Promise<void>
getMeta, // () => Promise<Meta | null>
isValidZipcode,// (zip: string) => boolean ← fetch しない簡易検証
configure, // シングルトンに options を渡し直す
} from "@jpzip/jpzip";
// 単発検索 — /p/231.json を 1 回 fetch → L1 にキャッシュ
const e = await lookup("2310017");
// グループ — 3桁=1 file / 2桁=10 並列 / 1桁=/g/{n}.json
const dict = await lookupGroup("23");
// 全件 preload — /g/0..9 を並列 fetch して L1 を温める
await preload({ scope: "all" });
// 以降の lookup() / lookupGroup() はネットワーク不要 JpzipClient (明示的なインスタンス)
テスト、マルチ環境、独立した永続キャッシュなどが必要な場合は new JpzipClient() を直接作ります。
import { JpzipClient } from "@jpzip/jpzip";
const client = new JpzipClient({
// すべてのオプションは省略可
baseUrl: "https://jpzip.nadai.dev", // デフォルト
memoryCacheSize: 100, // L1 LRU の prefix 数
cache: undefined, // L2 (後述)
fetch: globalThis.fetch, // テストで差し替え可
onSpecMismatch: ({ expected, received }) =>
console.warn(`spec mismatch: ${received}`),
});
const e = await client.lookup("1500001");
await client.refresh(); // L1/L2 を破棄して meta を再取得 永続キャッシュ (L2)
cache オプションに PersistentCache インタフェースを満たすオブジェクトを渡すと、
起動をまたいでデータが保持されます。標準実装は意図的に同梱していません — 環境に合わせて選んでください。
interface PersistentCache {
get(key: string): Promise<Uint8Array | null>;
set(key: string, value: Uint8Array, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
} ブラウザでは IndexedDB、Node ではファイル、Cloudflare では KV / Cache API、Redis なども差し込めます。
// 例: Node.js のシンプルなファイルキャッシュ
import { readFile, writeFile, unlink, rm } from "node:fs/promises";
import { createHash } from "node:crypto";
import { JpzipClient, PersistentCache } from "@jpzip/jpzip";
const dir = "./.jpzip-cache";
const path = (k: string) =>
`${dir}/${createHash("sha1").update(k).digest("hex")}.bin`;
const cache: PersistentCache = {
async get(k) { try { return await readFile(path(k)); } catch { return null; } },
async set(k, v) { await writeFile(path(k), v); },
async delete(k) { await unlink(path(k)).catch(() => {}); },
async clear() { await rm(dir, { recursive: true, force: true }); },
};
const client = new JpzipClient({ cache });
await client.preload({ scope: "all" });
// → 全件 L2 に永続化、次回起動でもネットワーク不要 Edge / Serverless 環境での注意
- Cloudflare Workers / Vercel Edge ではファイル L2 は使えません。
caches.defaultや Workers KV をラップしたPersistentCache実装を渡してください。 -
短命プロセスでは L1 がリクエスト間で再利用されない可能性が高いので、
preloadは init 時に一度だけ実行する設計にします。 -
getMeta()は SDK 内で 1 度しか呼ばれません (キャッシュ) — 毎リクエストで叩かないでください。
import 'package:jpzip/jpzip.dart';
final e = await lookup('2310017');
print('${e?.prefecture} ${e?.city}');
// 神奈川県 横浜市中区
pub.dev パッケージ jpzip。Dart 3.0+ / Flutter / CLI / Web 共通。package:http ベース、
内部で per-prefix 並列 fetch。JpzipClient は close() 必須。
トップレベル関数 (デフォルトクライアント)
import 'package:jpzip/jpzip.dart';
final e = await lookup('2310017'); // ZipcodeEntry?
final dict = await lookupGroup('23'); // 1-3 桁
final all = await lookupAll();
await preload('all');
final meta = await getMeta();
final ok = isValidZipcode('2310017'); JpzipClient + Options
import 'package:jpzip/jpzip.dart';
final client = JpzipClient(
baseURL: 'https://jpzip.nadai.dev',
memoryCacheSize: 200,
cache: myCache, // L2 (省略可)
onSpecMismatch: (exp, recv) => print('spec mismatch: $exp vs $recv'),
);
final e = await client.lookup('1500001');
await client.preload('all');
client.close(); // 内部 http.Client を閉じる Cache 抽象クラス
abstract class Cache {
Future<List<int>?> get(String key);
Future<void> set(String key, List<int> value);
Future<void> delete(String key);
Future<void> clear();
} ファイル / SharedPreferences / Hive / sqflite / IndexedDB など、任意の実装を渡せます。
Flutter / Edge 環境での注意
- Flutter アプリでは
JpzipClientをmain()で 1 度作って provider 経由で共有。preloadを初回起動でバックグラウンド実行するとオフライン即動作。 - Web (dart2js / dart2wasm) では
BrowserClientが自動選択され、CORS は CDN 側で許可済み。
import jpzip "github.com/jpzip/go"
ctx := context.Background()
e, err := jpzip.Lookup(ctx, "2310017")
// e.Prefecture, e.City, e.Towns[0].Town モジュール github.com/jpzip/go。Go 1.21+ で動作、依存ゼロ (標準ライブラリのみ)。
パッケージ関数 (シングルトン)
import jpzip "github.com/jpzip/go"
ctx := context.Background()
// 単発検索
e, err := jpzip.Lookup(ctx, "2310017")
if err != nil { return err }
if e == nil { /* 該当なし */ }
// その他の関数
dict, err := jpzip.LookupGroup(ctx, "23") // 1-3桁
all, err := jpzip.LookupAll(ctx) // 内部で /g/0..9 並列
err := jpzip.Preload(ctx, "all") // 全件キャッシュへ
meta, _ := jpzip.GetMeta(ctx) // /meta.json (1 回だけ)
ok := jpzip.IsValidZipcode("2310017") // 形式チェックのみ Client + Option パターン
client := jpzip.New(
jpzip.WithBaseURL("https://jpzip.nadai.dev"),
jpzip.WithHTTPClient(&http.Client{ Timeout: 30 * time.Second }),
jpzip.WithMemoryCacheSize(100), // L1 LRU の prefix 数
jpzip.WithCache(myFileCache), // L2 (jpzip.Cache を満たす任意の実装)
jpzip.OnSpecMismatch(func(expected, received string) {
log.Printf("spec mismatch: %s", received)
}),
)
e, err := client.Lookup(ctx, "1500001")
err := client.Refresh(ctx) // L1/L2 破棄 + meta 再取得 Cache インタフェース
L2 を有効化するには以下のインタフェースを満たす実装を WithCache に渡します。
type Cache interface {
Get(ctx context.Context, key string) ([]byte, bool, error)
Set(ctx context.Context, key string, v []byte) error
Delete(ctx context.Context, key string) error
Clear(ctx context.Context) error
} // 例: ローカルファイル実装
type fileCache struct{ dir string }
func (c *fileCache) Get(_ context.Context, k string) ([]byte, bool, error) {
b, err := os.ReadFile(filepath.Join(c.dir, hash(k)))
if errors.Is(err, os.ErrNotExist) { return nil, false, nil }
return b, err == nil, err
}
// Set / Delete / Clear は同様…
client := jpzip.New(jpzip.WithCache(&fileCache{dir: "/var/cache/jpzip"}))
client.Preload(ctx, "all") Edge / Serverless 環境での注意
-
Cloudflare Workers / 各種 FaaS では
os.ReadFileベースの L2 は使えません。jpzip.Cacheインタフェースを満たす Workers KV / Redis / S3 ラッパーを渡してください。 -
短命プロセスでは L1 がリクエスト間で再利用されません。
init()または最初のリクエスト時にPreloadを 1 度だけ呼ぶ設計が安全です。 -
GetMetaは SDK 内で 1 回しかネットワークに出ません — 毎リクエストで叩かないようにしてください。
use function Jpzip\lookup;
$e = lookup("2310017");
if ($e) echo "{$e->prefecture} {$e->city}";
// 神奈川県 横浜市中区
composer パッケージ jpzip/jpzip。PHP 8.2+、Guzzle 7 + readonly classes。2 桁 fan-out は Guzzle
Pool で並列。
名前空間関数 (シングルトン)
use function Jpzip\{lookup, lookupGroup, lookupAll, preload, getMeta, isValidZipcode};
$e = lookup("2310017"); // ?ZipcodeEntry
$dict = lookupGroup("23"); // 1-3 桁、2 桁は 10 並列
$all = lookupAll();
preload("all");
$ok = isValidZipcode("2310017"); Jpzip\Client (明示的なインスタンス)
use Jpzip\Client;
$client = new Client(
baseUrl: "https://jpzip.nadai.dev",
memoryCacheSize: 100,
cache: $myCache, // L2 (省略可)
onSpecMismatch: fn($exp, $recv) => error_log("spec: $recv"),
);
$e = $client->lookup("1500001");
$client->refresh(); CacheInterface
namespace Jpzip;
interface CacheInterface {
public function get(string $key): ?string;
public function set(string $key, string $value): void;
public function delete(string $key): void;
public function clear(): void;
} Edge / Serverless 環境での注意
- FPM / Laravel Octane / Swoole などの長命プロセスでは L1 LRU が完全に効く。
- RoadRunner や Bref (Lambda) では、ワーカーごとに
preload1 回が定石。
from jpzip import lookup
e = lookup("2310017")
if e:
print(e.prefecture, e.city, e.towns[0].town)
# 神奈川県 横浜市中区 本町
PyPI パッケージ名 jpzip。Python 3.10+、httpx 経由で sync / async 両対応、frozen
dataclass で型付き。
関数 API (シングルトン)
from jpzip import (
lookup, # (zip: str) -> ZipcodeEntry | None
lookup_group, # (prefix: str) -> dict[str, ZipcodeEntry]
lookup_all, # () -> dict[str, ZipcodeEntry]
preload, # (scope: str) -> None
get_meta, # () -> Meta | None
is_valid_zipcode,# (s: str) -> bool
)
e = lookup("2310017")
dict_ = lookup_group("23") # 2 桁は 10 並列 fetch
preload("all") # 全件 L1 を温める JpzipClient (明示的なインスタンス)
テスト・複数環境・L2 キャッシュなどが必要な場合は JpzipClient (sync) / AsyncJpzipClient
(async) を直接構築します。どちらも context manager 対応。
from jpzip import JpzipClient, AsyncJpzipClient
with JpzipClient(
base_url="https://jpzip.nadai.dev",
memory_cache_size=100,
cache=my_cache, # L2 (省略可)
on_spec_mismatch=lambda exp, recv: print(f"spec: {recv}"),
) as client:
e = client.lookup("1500001")
client.refresh() # L1/L2 を破棄
# async 版
async with AsyncJpzipClient() as client:
e = await client.lookup("2310017") 永続キャッシュ (L2)
from jpzip import Cache # Protocol (sync) — runtime_checkable
from jpzip import AsyncCache # Protocol (async)
class FileCache:
def get(self, key: str) -> bytes | None: ...
def set(self, key: str, value: bytes) -> None: ...
def delete(self, key: str) -> None: ...
def clear(self) -> None: ...
Redis / SQLite / S3 など任意の実装を cache= に渡せます。リポジトリは github.com/jpzip/python。
Edge / Serverless 環境での注意
- AWS Lambda / Cloud Functions では L1 がリクエスト間で生きるかどうかが warm 起動次第。
preloadはモジュールトップで 1 回。 get_meta()の結果はクライアント単位でキャッシュされるので、毎リクエストで叩かない。
require "jpzip"
e = Jpzip.lookup("2310017")
if e
puts "#{e.prefecture} #{e.city} #{e.towns[0].town}"
# 神奈川県 横浜市中区 本町
end
gem 名 jpzip。Ruby 3.2+、依存ゼロ (stdlib の Net::HTTP のみ)。型は Data.define の immutable な値オブジェクト。
モジュール関数 (シングルトン)
require "jpzip"
e = Jpzip.lookup("2310017") # ZipcodeEntry | nil
dict = Jpzip.lookup_group("23") # 1-3 桁
all_ = Jpzip.lookup_all # /g/0..9 並列
Jpzip.preload("all")
meta = Jpzip.meta # /meta.json
ok = Jpzip.valid_zipcode?("2310017") Jpzip::Client (明示的なインスタンス)
require "jpzip"
client = Jpzip::Client.new(
base_url: "https://jpzip.nadai.dev",
memory_cache_size: 100,
cache: my_cache, # L2 (省略可)
on_spec_mismatch: ->(exp, recv) { warn "spec: #{recv}" },
)
e = client.lookup("1500001")
client.refresh # L1/L2 を破棄 Cache インタフェース
L2 を有効化するには Jpzip::Cache を継承し以下を実装します。
class FileCache < Jpzip::Cache
def get(key); ...; end # String | nil
def set(key, value); ...; end
def delete(key); ...; end
def clear; ...; end
end Edge / Serverless 環境での注意
- AWS Lambda Ruby runtime / Heroku で動作。
Threadベースの fan-out を使用 (10 並列)。 - Sidekiq などのワーカーで起動時に
preloadしておくと、ジョブ実行時のレイテンシが消える。
use jpzip::JpzipClient;
let client = JpzipClient::builder().build();
let e = client.lookup("2310017").await?;
// → Some(ZipcodeEntry { prefecture: "神奈川県", city: "横浜市中区", .. })
crate 名 jpzip。Rust 1.75+、tokio + reqwest (rustls) + serde
。
関数 API (シングルトン)
use jpzip::{lookup, lookup_group, lookup_all, preload, get_meta, is_valid_zipcode};
let e = jpzip::lookup("2310017").await?;
let dict = jpzip::lookup_group("23").await?; // 1-3 桁
let all = jpzip::lookup_all().await?; // /g/0..9 並列
jpzip::preload("all").await?;
let ok = jpzip::is_valid_zipcode("2310017"); // 形式チェック JpzipClient + Builder
use jpzip::JpzipClient;
use std::sync::Arc;
let client = JpzipClient::builder()
.base_url("https://jpzip.nadai.dev")
.memory_cache_size(100)
.cache(Arc::new(my_cache)) // L2 (省略可)
.on_spec_mismatch(Arc::new(|exp, recv| {
eprintln!("spec mismatch: {recv}");
}))
.build();
let e = client.lookup("1500001").await?;
client.refresh().await?; Cache trait
#[async_trait::async_trait]
pub trait Cache: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, Error>;
async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), Error>;
async fn delete(&self, key: &str) -> Result<(), Error>;
async fn clear(&self) -> Result<(), Error>;
} Edge / Serverless 環境での注意
- Lambda / Workers (Rust 経由) では
tokioシングルスレッド runtime を共有。JpzipClientはClone可能なのでArc不要。 reqwestはrustls-tlsfeature を選択。native-tlsはクロスコンパイル時に詰まることがあります。
import Jpzip
if let e = try await lookup("2310017") {
print(e.prefecture, e.city, e.towns[0].town)
// 神奈川県 横浜市中区 本町
}
SwiftPM パッケージ jpzip。Swift 5.9+、iOS 15+ / macOS 12+ / tvOS 15+ / watchOS 8+。actor
ベースで Sendable 安全、依存ゼロ (URLSession のみ)。
トップレベル関数 (シングルトン)
import Jpzip
let e = try await lookup("2310017") // ZipcodeEntry?
let dict = try await lookupGroup("23") // 1-3 桁
let all = try await lookupAll()
try await preload("all")
let meta = try await getMeta()
let ok = isValidZipcode("2310017") JpzipClient (actor)
import Jpzip
let client = JpzipClient(
baseURL: "https://jpzip.nadai.dev",
memoryCacheSize: 100,
cache: myCache, // L2 (省略可)
onSpecMismatch: { expected, received in
print("spec: \(received)")
}
)
let e = try await client.lookup("1500001")
try await client.refresh() Cache protocol
public protocol Cache: Sendable {
func get(_ key: String) async throws -> Data?
func set(_ key: String, _ value: Data) async throws
func delete(_ key: String) async throws
func clear() async throws
} Edge / Serverless 環境での注意
- iOS / macOS アプリでは
FileManagerを使った L2 をUserDefaults代わりに使うと、起動時オフラインから即動く。 actor隔離なので await 呼び出しが必要。@MainActorから呼ぶと UI 更新と直結。
API 一覧 (lookup / lookupGroup / lookupAll / preload /
getMeta / refresh / isValidZipcode)、L1 LRU、オプショナル L2 キャッシュ、5xx
指数バックオフリトライ、データ version 変更時の自動 invalidate ― これらは全 SDK で共通実装されています。
キャッシュ戦略
SDK は 3 層のキャッシュを区別して管理します。デフォルト挙動を理解しておくと、preload / refresh のタイミングを判断しやすくなります。
| 層 | 目的 | サイズ目安 | デフォルト |
|---|---|---|---|
| L1 メモリ LRU | 同一プロセス内の重複 fetch 抑制 | ~1 MB | 常時 ON (内部) |
| L2 永続キャッシュ | preload / 起動高速化 | 最大 ~10 MB | OFF (ユーザー有効化) |
| L3 HTTP キャッシュ | ブラウザ / OS / fetch 層 | 環境依存 | Cache-Control 準拠 |
月次のデータ更新は /meta.json の version 変化で検知し、SDK は L1 と L2 を自動で
invalidate します。明示的に再取得したい場合は refresh() を呼んでください。
MCP server
Claude / 任意の Model Context Protocol クライアントから日本の郵便番号を検索できる stdio サーバー @jpzip/mcp-server-jpzip を別パッケージとして提供しています。背後は同じ jpzip.nadai.dev の静的データなので、SDK と同一の結果が返ります。
stateless で、Claude プロセスのメモリにのみキャッシュを持ちます (永続キャッシュなし)。
インストール
Claude Code:
claude mcp add jpzip -- npx -y @jpzip/mcp-server-jpzip Claude Desktop など mcp.json を直接編集する場合:
{
"mcpServers": {
"jpzip": {
"command": "npx",
"args": ["-y", "@jpzip/mcp-server-jpzip"]
}
}
} 提供 Tool
| Tool | 引数 | 用途 |
|---|---|---|
lookup_zipcode | zipcode: string (7 桁、ハイフン許容) | 郵便番号 → 住所 (漢字 / カナ / ローマ字 + JIS / 総務省コード) |
search_by_address | query: string, limit?: int (1–200、既定 20) | 住所文字列 → 郵便番号候補 (漢字 / カナ / ローマ字横断、空白無視の部分一致) |
list_cities_in_prefecture | prefecture: string | 都道府県 → 市区町村一覧 (総務省コード付き) |
get_metadata | — | データ version・件数・生成時刻 |
動作モデル
-
lookup_zipcodeは対応する 3 桁 prefix (~10 KB) のみを CDN から取得し、SDK の L1 LRU にキャッシュします。 -
search_by_address/list_cities_in_prefectureは初回呼び出し時に全件 (~25 MB) を CDN から取得しメモリ保持。 同一 MCP プロセス内の以降の呼び出しは即時です。 - 永続キャッシュは持ちません (stateless)。Claude の再起動でメモリは破棄され、次回必要になった時点で再取得します。
- 駅・路線・事業所情報は jpzip データセットに含まれません (郵便番号 ⇄ 住所のみ)。 検索は 1 言語 (漢字 / カナ / ローマ字) 内の連続部分一致のみ対応します。
ソース: github.com/jpzip/mcp / npm: @jpzip/mcp-server-jpzip。
プロトコル仕様
SDK を使わず raw HTTP で叩く場合や、別言語の SDK を実装する場合の参照です。spec の正本は github.com/jpzip/spec にあります。
エンドポイント
| パス | 内容 | サイズ目安 |
|---|---|---|
/meta.json | バージョン & 統計 | ~10 KB |
/g/{1}.json | 1 桁プレフィックスの全エントリ | 3〜4 MB |
/p/{3}.json | 3 桁プレフィックスの全エントリ | ~10 KB |
/all.json は意図的にありません — 全件で 25 MiB を超え Cloudflare Pages の上限を超過するためです。
全件が必要なときは /g/0.json 〜 /g/9.json を並列取得してマージします
(SDK の lookupAll / preload はこれを自動で実行します)。
curl https://jpzip.nadai.dev/p/231.json \
| jq '."2310017"' スキーマ
/g/* と /p/* は同じ構造のフラットな辞書。キーは 7 桁の zipcode、値は ZipcodeEntry。
{
"prefecture": "神奈川県",
"prefecture_kana": "カナガワケン",
"prefecture_roma": "Kanagawa",
"prefecture_code": "14", // JIS X 0401 (2桁)
"city": "横浜市中区",
"city_kana": "ヨコハマシナカク",
"city_roma": "Yokohama Shi Naka Ku",
"city_code": "14104", // 総務省コード (5桁)
"towns": [
{ "town": "本町", "kana": "ホンチョウ", "roma": "Honcho" }
]
}
1 つの zipcode に複数町域が紐づく場合 (例: 京都の通り名) は towns 配列が複数要素になります。
「以下に掲載がない場合」のような特記事項は towns[].note に正規化されます。
HTTP 仕様
- Method: GET のみ (クエリパラメータなし)
- Content-Type:
application/json; charset=utf-8 - Encoding: gzip / brotli (Cloudflare 自動)
- Cache-Control:
public, max-age=86400(24h) - CORS:
Access-Control-Allow-Origin: * - 404: 実在しない prefix。SDK は「該当なし」として扱う
- 5xx / network: SDK は指数バックオフで最大 3 回リトライ
バージョニング
- spec_version (
1.0など): プロトコルのバージョン。SemVer 準拠。v1 系列の中では 既存フィールドの削除・型変更は行わない。 - version (
2026-05など): データの月次バージョン。互換性とは無関係、データの 新鮮さの指標。
ライセンス
- 仕様書 / SDK / ETL: MIT
- 配信データ: Public Domain 相当 (元データは日本郵便)
商用利用・再配布・改変、いずれも自由です。