Introduction
jpzip is a set of static JSON files served from https://jpzip.nadai.dev. Each language SDK is a thin
wrapper around that data — it fetches, manages memory / persistent caches, and lets you go all the way offline.
Install
# 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-second quickstart
Minimal example — the singleton initialises itself, so you can call it directly. Switch languages in the SDK section below.
SDK
Use the language tabs. The hash in the URL (e.g. #go-client) jumps directly to a language.
import { lookup } from "@jpzip/jpzip";
const e = await lookup("2310017");
if (e) console.log(e.prefecture, e.city, e.towns[0].town);
// 神奈川県 横浜市中区 本町 (Kanagawa, Yokohama Naka-ku, Honcho)
npm package @jpzip/jpzip. ESM / CJS dual, fully typed, runs on Node 18+, major browsers, Bun, Deno
and Cloudflare Workers.
Functional API (singleton)
When one app needs one client, the functional API is shortest. It shares a lazily-initialised JpzipClient under the hood, so the L1 in-memory cache always applies.
import {
lookup, // (zip: string) => Promise<ZipcodeEntry | null>
lookupGroup, // (prefix: 1-3 digits) => Promise<ZipcodeDict>
lookupAll, // () => Promise<ZipcodeDict> (parallel /g/0..9)
preload, // ({ scope: 'all' | '0' | '23' | '231' }) => Promise<void>
getMeta, // () => Promise<Meta | null>
isValidZipcode,// (zip: string) => boolean ← format check, no fetch
configure, // re-apply options to the singleton
} from "@jpzip/jpzip";
// Single lookup — fetch /p/231.json once, then L1 hits
const e = await lookup("2310017");
// Group — 3-digit=1 file / 2-digit=10 in parallel / 1-digit=/g/{n}.json
const dict = await lookupGroup("23");
// Full preload — fetch /g/0..9 in parallel and warm the L1
await preload({ scope: "all" });
// Subsequent lookup() / lookupGroup() need no network JpzipClient (explicit instance)
For tests, multi-environment setups, or independent persistent caches, instantiate new JpzipClient() yourself.
import { JpzipClient } from "@jpzip/jpzip";
const client = new JpzipClient({
// every option is optional
baseUrl: "https://jpzip.nadai.dev", // default
memoryCacheSize: 100, // number of prefixes in the L1 LRU
cache: undefined, // L2 (below)
fetch: globalThis.fetch, // swappable in tests
onSpecMismatch: ({ expected, received }) =>
console.warn(`spec mismatch: ${received}`),
});
const e = await client.lookup("1500001");
await client.refresh(); // drop L1/L2 and re-fetch meta Persistent cache (L2)
Pass any object that satisfies the PersistentCache interface as the cache option
and data survives across process restarts. No default implementation ships with the SDK — pick one that
suits your runtime.
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 in browsers, files on Node, KV / Cache API on Cloudflare, Redis — anything goes.
// Example: a simple Node.js file cache
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" });
// → entire dataset is L2-persisted; next start works offline Edge / Serverless notes
- Cloudflare Workers / Vercel Edge can’t use file-based L2. Wrap
caches.defaultor Workers KV in aPersistentCacheinstead. -
On short-lived runtimes the L1 won’t reliably survive between requests, so call
preloadonce at init time. -
getMeta()is fetched once and cached inside the SDK — don’t call it per request.
import 'package:jpzip/jpzip.dart';
final e = await lookup('2310017');
print('${e?.prefecture} ${e?.city}');
// 神奈川県 横浜市中区 (Kanagawa, Yokohama Naka-ku)
pub.dev package jpzip. Dart 3.0+, runs on Flutter / CLI / Server / Web. Built on
package:http; per-prefix parallel fetch. Close JpzipClient explicitly when done.
Top-level functions (default client)
import 'package:jpzip/jpzip.dart';
final e = await lookup('2310017'); // ZipcodeEntry?
final dict = await lookupGroup('23'); // 1-3 digits
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 (optional)
onSpecMismatch: (exp, recv) => print('spec mismatch: $exp vs $recv'),
);
final e = await client.lookup('1500001');
await client.preload('all');
client.close(); // closes the internal http.Client Cache abstract class
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();
} Plug in any storage — file system / SharedPreferences / Hive / sqflite / IndexedDB.
Flutter / Edge notes
- In Flutter apps, create one
JpzipClientinmain()and share via a provider. Runpreloadin the background on first launch to be offline-ready instantly. - On Web (dart2js / dart2wasm),
BrowserClientis selected automatically. CORS is enabled on the CDN.
import jpzip "github.com/jpzip/go"
ctx := context.Background()
e, err := jpzip.Lookup(ctx, "2310017")
// e.Prefecture, e.City, e.Towns[0].Town Module github.com/jpzip/go. Runs on Go 1.21+, zero dependencies (standard library only).
Package functions (singleton)
import jpzip "github.com/jpzip/go"
ctx := context.Background()
// Single lookup
e, err := jpzip.Lookup(ctx, "2310017")
if err != nil { return err }
if e == nil { /* not found */ }
// Other functions
dict, err := jpzip.LookupGroup(ctx, "23") // 1-3 digits
all, err := jpzip.LookupAll(ctx) // parallel /g/0..9
err := jpzip.Preload(ctx, "all") // cache full dataset
meta, _ := jpzip.GetMeta(ctx) // /meta.json (once)
ok := jpzip.IsValidZipcode("2310017") // format check only Client + Option pattern
client := jpzip.New(
jpzip.WithBaseURL("https://jpzip.nadai.dev"),
jpzip.WithHTTPClient(&http.Client{ Timeout: 30 * time.Second }),
jpzip.WithMemoryCacheSize(100), // number of prefixes in the L1 LRU
jpzip.WithCache(myFileCache), // L2 (any jpzip.Cache impl)
jpzip.OnSpecMismatch(func(expected, received string) {
log.Printf("spec mismatch: %s", received)
}),
)
e, err := client.Lookup(ctx, "1500001")
err := client.Refresh(ctx) // drop L1/L2 + re-fetch meta Cache interface
To enable L2, pass any implementation of the following interface to 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
} // Example: local file implementation
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 are similar…
client := jpzip.New(jpzip.WithCache(&fileCache{dir: "/var/cache/jpzip"}))
client.Preload(ctx, "all") Edge / Serverless notes
-
On Cloudflare Workers / FaaS, file-based L2 won’t work — wrap Workers KV / Redis / S3 in
jpzip.Cache. -
On short-lived processes, L1 won’t survive between requests. Run
Preloadonce frominit()or your first request. -
GetMetahits the network once per client — don’t call it per request.
use function Jpzip\lookup;
$e = lookup("2310017");
if ($e) echo "{$e->prefecture} {$e->city}";
// 神奈川県 横浜市中区
composer package jpzip/jpzip. PHP 8.2+, Guzzle 7 + readonly classes. 2-digit fan-out uses
Guzzle Pool.
Namespace functions (singleton)
use function Jpzip\{lookup, lookupGroup, lookupAll, preload, getMeta, isValidZipcode};
$e = lookup("2310017"); // ?ZipcodeEntry
$dict = lookupGroup("23"); // 1-3 digits; 2-digit fans out 10
$all = lookupAll();
preload("all");
$ok = isValidZipcode("2310017"); Jpzip\Client (explicit instance)
use Jpzip\Client;
$client = new Client(
baseUrl: "https://jpzip.nadai.dev",
memoryCacheSize: 100,
cache: $myCache, // L2 (optional)
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 notes
- On long-lived runtimes (FPM / Laravel Octane / Swoole), the L1 LRU is fully effective.
- On RoadRunner / Bref (Lambda), call
preloadonce per worker on boot.
from jpzip import lookup
e = lookup("2310017")
if e:
print(e.prefecture, e.city, e.towns[0].town)
# 神奈川県 横浜市中区 本町
PyPI package jpzip. Python 3.10+, sync and async via httpx, typed with frozen
dataclasses.
Functional API (singleton)
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-digit fans out 10 in parallel
preload("all") # warm the full L1 JpzipClient (explicit instance)
For tests, multi-env apps, or L2 caches, instantiate JpzipClient (sync) or
AsyncJpzipClient (async) directly. Both work as context managers.
from jpzip import JpzipClient, AsyncJpzipClient
with JpzipClient(
base_url="https://jpzip.nadai.dev",
memory_cache_size=100,
cache=my_cache, # L2 (optional)
on_spec_mismatch=lambda exp, recv: print(f"spec: {recv}"),
) as client:
e = client.lookup("1500001")
client.refresh() # drop L1/L2
# async variant
async with AsyncJpzipClient() as client:
e = await client.lookup("2310017") Persistent cache (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: ... Plug in Redis / SQLite / S3 / anything. Source at github.com/jpzip/python.
Edge / Serverless notes
- On AWS Lambda / Cloud Functions, L1 survival depends on warm starts. Call
preloadonce at module top. get_meta()is cached per client — don’t call it per request.
require "jpzip"
e = Jpzip.lookup("2310017")
if e
puts "#{e.prefecture} #{e.city} #{e.towns[0].town}"
# 神奈川県 横浜市中区 本町
end
gem jpzip. Ruby 3.2+, zero dependencies (stdlib Net::HTTP only). Values are immutable
via Data.define.
Module functions (singleton)
require "jpzip"
e = Jpzip.lookup("2310017") # ZipcodeEntry | nil
dict = Jpzip.lookup_group("23") # 1-3 digits
all_ = Jpzip.lookup_all # /g/0..9 parallel
Jpzip.preload("all")
meta = Jpzip.meta # /meta.json
ok = Jpzip.valid_zipcode?("2310017") Jpzip::Client (explicit instance)
require "jpzip"
client = Jpzip::Client.new(
base_url: "https://jpzip.nadai.dev",
memory_cache_size: 100,
cache: my_cache, # L2 (optional)
on_spec_mismatch: ->(exp, recv) { warn "spec: #{recv}" },
)
e = client.lookup("1500001")
client.refresh # drop L1/L2 Cache interface
To enable L2, inherit from Jpzip::Cache and implement:
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 notes
- Runs on AWS Lambda Ruby runtime / Heroku. Internal fan-out uses
Thread(10 concurrent). - For Sidekiq workers, call
preloadat boot so per-job latency vanishes.
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.
Functional API (singleton)
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 digits
let all = jpzip::lookup_all().await?; // /g/0..9 parallel
jpzip::preload("all").await?;
let ok = jpzip::is_valid_zipcode("2310017"); // format check 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 (optional)
.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 notes
- On Lambda / Workers (via Rust) tokio shares a single-threaded runtime.
JpzipClientisClone, so noArcneeded. - Prefer
reqwestwithrustls-tls;native-tlscan be painful to cross-compile.
import Jpzip
if let e = try await lookup("2310017") {
print(e.prefecture, e.city, e.towns[0].town)
// 神奈川県 横浜市中区 本町
}
SwiftPM package jpzip. Swift 5.9+, iOS 15+ / macOS 12+ / tvOS 15+ / watchOS 8+. actor-based,
Sendable-safe, zero dependencies (URLSession only).
Top-level functions (singleton)
import Jpzip
let e = try await lookup("2310017") // ZipcodeEntry?
let dict = try await lookupGroup("23") // 1-3 digits
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 (optional)
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 notes
- On iOS / macOS apps a
FileManager-backed L2 makes cold launches work offline immediately. - Since
actorisolates state, call sites needawait. Using@MainActorintegrates cleanly with UI updates.
The shared surface (lookup / lookupGroup / lookupAll / preload /
getMeta / refresh / isValidZipcode), the L1 LRU, the optional L2 cache,
5xx exponential-backoff retries and version-change auto-invalidation are implemented identically across all SDKs.
Cache strategy
The SDK distinguishes three cache layers. Knowing the defaults makes it easy to reason about when to call preload / refresh.
| Layer | Purpose | Typical size | Default |
|---|---|---|---|
| L1 in-memory LRU | Suppress duplicate fetches within one process | ~1 MB | Always on (internal) |
| L2 persistent cache | preload / fast boot | Up to ~10 MB | Off (user-enabled) |
| L3 HTTP cache | Browser / OS / fetch layer | Environment-dependent | Follows Cache-Control |
Monthly data updates are detected from the version field in /meta.json; the SDK
automatically invalidates L1 and L2. To re-fetch explicitly, call refresh().
MCP server
A separate stdio server, @jpzip/mcp-server-jpzip, lets Claude and any other Model Context Protocol
client look up Japanese postal codes. It is backed by the same jpzip.nadai.dev static data, so the
results match the SDKs exactly. The process is stateless and keeps cached data only in memory — no on-disk cache.
Install
Claude Code:
claude mcp add jpzip -- npx -y @jpzip/mcp-server-jpzip Claude Desktop, or any client where you edit mcp.json directly:
{
"mcpServers": {
"jpzip": {
"command": "npx",
"args": ["-y", "@jpzip/mcp-server-jpzip"]
}
}
} Tools
| Tool | Arguments | Purpose |
|---|---|---|
lookup_zipcode | zipcode: string (7 digits, hyphens allowed) | Zipcode → address (kanji / kana / romaji + JIS / MIC codes) |
search_by_address | query: string, limit?: int (1–200, default 20) | Free-text address → candidate zipcodes (matches across kanji / kana / romaji, whitespace ignored) |
list_cities_in_prefecture | prefecture: string | Prefecture → list of municipalities with their MIC city codes |
get_metadata | — | Data version, entry count, build timestamp |
Runtime model
-
lookup_zipcodeonly fetches the matching 3-digit prefix file (~10 KB) and stores it in the SDK’s L1 LRU. -
search_by_address/list_cities_in_prefecturefetch the full dataset (~25 MB) on first call and keep it in memory; subsequent calls within the same MCP process are instant. - No on-disk cache. When Claude restarts, the in-memory data is dropped and re-fetched on demand.
-
jpzip data does not include stations, train lines, or per-business zipcodes (zipcode ⇄ address only). The
address search is single-language substring only — e.g. a mixed query like
Yokohama Honchothat skips the ward name will not match.
Source: github.com/jpzip/mcp / npm: @jpzip/mcp-server-jpzip.
Protocol spec
Reference for raw HTTP usage or for writing your own SDK. The authoritative spec lives at github.com/jpzip/spec.
Endpoints
| Path | Contents | Approx. size |
|---|---|---|
/meta.json | Version & stats | ~10 KB |
/g/{1}.json | All entries for a 1-digit prefix | 3–4 MB |
/p/{3}.json | All entries for a 3-digit prefix | ~10 KB |
There is intentionally no /all.json — at over 25 MiB it would exceed Cloudflare Pages limits.
When you need the whole dataset, fetch /g/0.json through /g/9.json in parallel and
merge (the SDK’s lookupAll / preload do this for you).
curl https://jpzip.nadai.dev/p/231.json \
| jq '."2310017"' Schema
/g/* and /p/* are flat dictionaries with the same shape. Keys are 7-digit zipcodes and
values are ZipcodeEntry.
{
"prefecture": "神奈川県",
"prefecture_kana": "カナガワケン",
"prefecture_roma": "Kanagawa",
"prefecture_code": "14", // JIS X 0401 (2 digits)
"city": "横浜市中区",
"city_kana": "ヨコハマシナカク",
"city_roma": "Yokohama Shi Naka Ku",
"city_code": "14104", // MIC municipality code (5 digits)
"towns": [
{ "town": "本町", "kana": "ホンチョウ", "roma": "Honcho" }
]
}
When one zipcode covers several town areas (e.g. Kyoto’s street-name addresses), the towns array
has multiple entries. Notes like “if not listed below” are normalised into towns[].note.
HTTP spec
- Method: GET only (no query parameters)
- Content-Type:
application/json; charset=utf-8 - Encoding: gzip / brotli (Cloudflare, automatic)
- Cache-Control:
public, max-age=86400(24h) - CORS:
Access-Control-Allow-Origin: * - 404: Prefix does not exist. SDKs treat this as “not found”.
- 5xx / network: SDKs retry up to 3 times with exponential backoff.
Versioning
- spec_version (e.g.
1.0): the protocol version, SemVer. Within v1.x no existing field is removed or type-changed. - version (e.g.
2026-05): the monthly data version. Unrelated to compatibility — it just tells you how fresh the data is.
License
- Spec / SDK / ETL: MIT
- Distributed data: effectively public domain (sourced from Japan Post)
Commercial use, redistribution and modification are all free. Attribution is not required (though appreciated).