郵便番号から住所を自動入力するには、7桁がそろった時点で郵便番号検索APIに問い合わせ、返ってきた都道府県・市区町村・町域を入力欄に入れます。 無料で登録なしに使える zipcloud の API なら、HTML とこの記事の JavaScript を貼るだけで動きます。

ただ、よく見かけるコードの多くは「半角7桁がキー入力されたとき」しか想定していません。実際の入力では、全角数字(100-0001)、ハイフン付き、貼り付け、スマホの自動入力、1つの郵便番号に町域が複数あるケース、該当なし、通信エラーが起きます。この記事のコードはそれらを全部扱い、Chromium で動かした結果を載せています。

確認環境:Chromium(Playwright 経由)、Node.js 22.22.2、Python 3.11。zipcloud の API 応答は 2026年10月10日に実際に取得したものを使っています(確認日 2026年10月10日)。

4つの方式の比較

方式 費用・登録 住所データの更新 向いている場面
zipcloud の郵便番号検索API 無料・登録なし 提供元が更新(確認日時点で2026年9月30日更新分) 小〜中規模のサイト。すぐ動かしたい
日本郵便「郵便番号・デジタルアドレス API」 無料・利用登録が必要 日本郵便が提供 業務システム。公式データで、漢字・カナ・ローマ字やデジタルアドレスも引きたい
YubinBango(JavaScript ライブラリ) 無料(MIT ライセンス) ライブラリ側の郵便番号データ フォームに class を付けるだけで済ませたい
日本郵便の郵便番号データ(CSV)を自前で持つ 無料 自分で毎月取り込む 外部サービスに依存したくない。大量に引く

迷ったら zipcloud から始め、業務システムで公式データが必要になったら日本郵便の API、外部に問い合わせたくない要件なら自前データ、という順に考えると決めやすくなります。どの方式でも、フォームの作り(入力の正規化、候補が複数あるときの選択、該当なしの案内)は同じです。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「郵便番号から住所を自動入力|無料APIの比較とJavaScript完成コード」

完成コード:HTML と JavaScript(zipcloud 版)

HTML

<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>郵便番号から住所を自動入力</title></head>
<body>
<form class="zip-form">
  <label>郵便番号 <input name="zip" inputmode="numeric" autocomplete="postal-code" maxlength="8" placeholder="100-0001"></label>
  <p class="zip-message" role="status" aria-live="polite"></p>
  <select name="town-choice" hidden></select>
  <label>都道府県 <input name="pref" autocomplete="address-level1"></label>
  <label>市区町村 <input name="city" autocomplete="address-level2"></label>
  <label>町域 <input name="town" autocomplete="address-line1"></label>
  <label>番地・建物名 <input name="street" autocomplete="address-line2"></label>
</form>
<script src="zip-autofill.js"></script>
</body>
</html>

autocomplete 属性を付けておくと、ブラウザの住所の自動入力とも共存できます。maxlength="8" はハイフン入りの8文字に合わせています。

JavaScript(zip-autofill.js)

// 郵便番号から住所を自動入力する(zipcloud の API を使用)
const API = 'https://zipcloud.ibsnet.co.jp/api/search';
const cache = new Map();
let controller = null;
let seq = 0;                                 // 最後の入力だけを画面に反映するための番号

// 全角数字・全角ハイフン・長音記号などを取り除いて7桁にそろえる
function normalizeZip(value) {
  return value
    .replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
    .replace(/[--―‐ー〒\s]/g, '');
}

async function searchAddress(zip) {
  if (cache.has(zip)) return cache.get(zip);
  controller?.abort();                       // 前の検索が残っていれば止める
  controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 5000);
  try {
    const res = await fetch(`${API}?zipcode=${zip}`, { signal: controller.signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const data = await res.json();
    if (data.status !== 200) throw new Error(data.message);
    const results = data.results ?? [];      // 該当なしは results が null
    cache.set(zip, results);
    return results;
  } finally {
    clearTimeout(timer);
  }
}

function setupZipAutofill(form) {
  const zip = form.querySelector('[name="zip"]');
  const pref = form.querySelector('[name="pref"]');
  const city = form.querySelector('[name="city"]');
  const town = form.querySelector('[name="town"]');
  const street = form.querySelector('[name="street"]');
  const msg = form.querySelector('.zip-message');
  const choice = form.querySelector('[name="town-choice"]');

  const fill = (r) => {
    pref.value = r.address1;
    city.value = r.address2;
    town.value = r.address3;
    street.focus();                          // 続きの番地へカーソルを送る
  };
  const clear = () => { pref.value = city.value = town.value = ''; };

  zip.addEventListener('input', async () => {
    const value = normalizeZip(zip.value);
    msg.textContent = '';
    choice.hidden = true;
    if (!/^\d{7}$/.test(value)) return;      // 7桁そろうまで何もしない
    zip.value = value.slice(0, 3) + '-' + value.slice(3);
    msg.textContent = '住所を検索しています…';
    const id = ++seq;
    try {
      const results = await searchAddress(value);
      if (id !== seq) return;                // 後から別の番号が入力された
      if (results.length === 0) {
        clear();                             // 前の検索で入った住所を消す
        msg.textContent = 'この郵便番号の住所が見つかりませんでした。住所を直接入力できます。';
        return;
      }
      msg.textContent = '';
      fill(results[0]);
      if (results.length > 1) {              // 1つの郵便番号に町域が複数ある
        choice.replaceChildren(...results.map((r, i) => new Option(r.address3, i)));
        choice.hidden = false;
        choice.onchange = () => fill(results[choice.value]);
        msg.textContent = `町域の候補が${results.length}件あります。選択欄から選べます。`;
      }
    } catch (e) {
      if (id !== seq) return;
      if (e.name === 'AbortError') {
        msg.textContent = '住所の検索に時間がかかっています。住所を直接入力できます。';
      } else {
        msg.textContent = '住所を検索できませんでした。住所を直接入力できます。';
      }
    }
  });
}

document.querySelectorAll('form.zip-form').forEach(setupZipAutofill);

このコードがしていること

処理 理由
input イベントで判定 keyup だと貼り付け・スマホの自動入力・音声入力で動かない
全角数字を半角に、ハイフン・長音記号・〒・空白を除去 「100-0001」「100ー0001」「〒100-0001」も7桁にそろえる
7桁そろうまで問い合わせない 1文字ごとに API を呼ばない
AbortController で前の検索を止め、5秒で打ち切る 打ち直したときに前の結果で上書きしない。応答が無いまま待たせない
入力ごとに番号(seq)を振る 遅れて届いた前の応答を画面に反映しない
結果を Map に保存 同じ番号を打ち直したときに再検索しない
results が null なら「見つかりませんでした」 zipcloud は該当なしのとき results: null を返す
候補が2件以上なら選択欄を出す 1つの郵便番号に複数の町域がある
入力後は番地欄へカーソルを移す 続きをそのまま入力できる
候補は new Option() で作る API の文字列を HTML として解釈させない

ブラウザで動かした結果

zipcloud の実際の応答(下の「API の応答」)をそのまま返すようにして、Chromium で入力を試しました。

全角で入力   {"zip":"100-0001","pref":"東京都","city":"千代田区","town":"千代田","msg":"","focus":"street"}
町域が複数   {"zip":"079-0177","pref":"北海道","city":"美唄市","town":"上美唄町協和","msg":"町域の候補が3件あります。選択欄から選べます。","choice":"上美唄町協和/上美唄町南/上美唄町","focus":"street"}
候補を選択   {"zip":"079-0177","pref":"北海道","city":"美唄市","town":"上美唄町南", ...}
該当なし     {"zip":"999-9999","pref":"","city":"","town":"","msg":"この郵便番号の住所が見つかりませんでした。住所を直接入力できます。","focus":"zip"}
2回目        {"zip":"100-0001","pref":"東京都","city":"千代田区","town":"千代田", ...}
APIを呼んだ回数 3
  • 全角の「100-0001」は「100-0001」に整えられて検索されました
  • 079-0177 は町域が3つあり、選択欄から「上美唄町南」に切り替えられました
  • 該当なしでは、前の検索で入った住所が消え、案内が出ました
  • 5回の入力に対して API の呼び出しは3回(2回目の 100-0001 は保存した結果を使用)

API の応答(zipcloud)

https://zipcloud.ibsnet.co.jp/api/search?zipcode=1000001 の応答です。

{
  "message": null,
  "results": [
    {
      "address1": "東京都",
      "address2": "千代田区",
      "address3": "千代田",
      "kana1": "トウキョウト",
      "kana2": "チヨダク",
      "kana3": "チヨダ",
      "prefcode": "13",
      "zipcode": "1000001"
    }
  ],
  "status": 200
}

町域が複数ある 0790177 では results が3件の配列になります。

{
  "message": null,
  "results": [
    { "address1": "北海道", "address2": "美唄市", "address3": "上美唄町協和", "prefcode": "1", "zipcode": "0790177" },
    { "address1": "北海道", "address2": "美唄市", "address3": "上美唄町南",   "prefcode": "1", "zipcode": "0790177" },
    { "address1": "北海道", "address2": "美唄市", "address3": "上美唄町",     "prefcode": "1", "zipcode": "0790177" }
  ],
  "status": 200
}

(読みやすさのため kana1〜kana3 を省いています)

項目 内容
status 正常時 200。パラメータの誤りは 400、API 内部のエラーは 500
message エラー時の内容(例:「必須パラメータが指定されていません。」)
results 該当なしは null、同じ郵便番号に複数あれば配列(既定の上限20件、limit で変更)
address1〜3 都道府県名・市区町村名・町域名
kana1〜3 上の読み(半角カナ)
prefcode JIS X 0401 の都道府県コード(1〜47)

パラメータは zipcode(7桁。ハイフン付きも可)、callback(JSONP で受け取るときの関数名)、limit(件数の上限)です。

都道府県をセレクトボックスにしている場合

<option value="13">東京都</option> のように値を都道府県コードにしておけば、pref.value = r.prefcode; で選べます。値を都道府県名にしているなら r.address1 を入れます。

「CORS エラー」が出たとき

ブラウザから直接呼ぶときに出やすいのが CORS エラーです。よくある原因は、axios の withCredentials: true や fetch の credentials: 'include' で Cookie 付きの送信になっていることです。Cookie 付きの送信では、相手が全オリジン許可(Access-Control-Allow-Origin: *)で応答していてもブラウザが受け取りを止めます。郵便番号の検索に Cookie は要らないので、この記事のコードのように付けずに呼びます。

それでも通らない環境では、次のどちらかにします。

  • JSONP で受け取る:callback パラメータに関数名を渡し、<script> タグで読み込む
  • 自分のサーバーを経由する:サーバー側で API を呼び、同じオリジンで画面に返す(日本郵便の API のように認証情報が必要なものは、この形が前提です)

JSONP の最小例です。

function searchAddressJsonp(zip) {
  return new Promise((resolve, reject) => {
    const name = 'zipcb_' + Date.now();
    const script = document.createElement('script');
    window[name] = (data) => {
      resolve(data.results ?? []);
      delete window[name];
      script.remove();
    };
    script.onerror = () => { reject(new Error('load error')); script.remove(); };
    script.src = `https://zipcloud.ibsnet.co.jp/api/search?zipcode=${zip}&callback=${name}`;
    document.head.appendChild(script);
  });
}

React で使う場合(カスタムフック)

import { useEffect, useState } from 'react';

type Address = { pref: string; city: string; town: string };

export function useZipAddress(zipInput: string) {
  const [candidates, setCandidates] = useState<Address[]>([]);
  const [error, setError] = useState('');

  useEffect(() => {
    const zip = zipInput
      .replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
      .replace(/[--―‐ー〒\s]/g, '');
    if (!/^\d{7}$/.test(zip)) { setCandidates([]); setError(''); return; }

    const controller = new AbortController();
    fetch(`https://zipcloud.ibsnet.co.jp/api/search?zipcode=${zip}`, { signal: controller.signal })
      .then((res) => res.json())
      .then((data) => {
        const list = (data.results ?? []).map((r: any) => ({
          pref: r.address1, city: r.address2, town: r.address3,
        }));
        setCandidates(list);
        setError(list.length === 0 ? 'この郵便番号の住所が見つかりませんでした' : '');
      })
      .catch((e) => { if (e.name !== 'AbortError') setError('住所を検索できませんでした'); });

    return () => controller.abort();   // 次の入力が来たら前の検索を止める
  }, [zipInput]);

  return { candidates, error };
}
const { candidates, error } = useZipAddress(zip);
useEffect(() => {
  if (candidates[0]) setAddress(candidates[0]);   // 1件目を入れ、2件以上なら選択肢を出す
}, [candidates]);

useEffect の後片付けで abort() しているので、打ち直したときに前の結果が後から入ることはありません。

ライブラリで済ませる:YubinBango

JavaScript を書かずに済ませたいなら、YubinBango(MIT ライセンス)が使えます。フォームと入力欄に決まった class を付け、スクリプトを1行読み込むだけです。全角数字やハイフンも受け付けます。

<script src="https://yubinbango.github.io/yubinbango/yubinbango.js" charset="UTF-8"></script>

<form class="h-adr">
  <span class="p-country-name" style="display:none;">Japan</span>
  郵便番号 <input type="text" class="p-postal-code" size="8" maxlength="8"><br>
  都道府県 <input type="text" class="p-region"><br>
  市区町村 <input type="text" class="p-locality"><br>
  町域 <input type="text" class="p-street-address"><br>
  番地・建物名 <input type="text" class="p-extended-address">
</form>
class 付ける場所
h-adr <form>
p-country-name 国名(Japan。非表示でよい)
p-postal-code 郵便番号の入力欄
p-region 都道府県
p-locality 市区町村
p-street-address 町域
p-extended-address 番地・建物名

町域が複数ある郵便番号の選択や、該当なしの案内を自分で作り込みたい場合は、この記事の完成コードのほうが調整しやすくなります。

公式データで引く:日本郵便「郵便番号・デジタルアドレス API」

日本郵便は2025年5月26日から「郵便番号・デジタルアドレス API」を無料で提供しています。郵便番号から都道府県・市区町村・町域を、漢字・カナ・ローマ字で取得でき、フリーワード検索や、7桁の英数字の「デジタルアドレス」から建物名まで含む住所の取得もできます。2026年3月には事業所のデジタルアドレスから会社名などを引く機能も追加されています。

利用には「ゆうID」と「郵便番号・デジタルアドレス for Biz アカウント」の登録、組織情報の登録が必要です。発行される認証情報を画面の JavaScript に書くと誰でも読めてしまうため、呼び出しは自社のサーバー側で行い、画面には結果だけを返す構成にします。画面側のコードは、この記事の searchAddress() の URL を自社サーバーの窓口に替えるだけで流用できます。

自前のデータで引く:日本郵便の郵便番号データ(CSV)

外部サービスに問い合わせたくない場合は、日本郵便が公開している郵便番号データ(全国一括で約12万件の CSV を zip 圧縮したもの、毎月更新)を取り込んで、自分のサーバーから配信します。

取り込むときに気をつける点が3つあります。

注意点 内容(日本郵便「郵便番号データの説明」より)
文字コード MS漢字コード(Shift_JIS)。UTF-8 として読むと文字化けする
「以下に掲載がない場合」 町域を特定しない行。住所欄に入れない
長い町域名は複数行に分割 漢字の町域部分が38文字を超えると、次の行に続きが入る

次の Python は、CSV を郵便番号の上3桁ごとの JSON に分けて書き出します(YubinBango と同じ分け方で、画面からは上3桁のファイルを1つ読むだけで済みます)。

"""日本郵便の KEN_ALL.CSV(Shift_JIS)を、郵便番号上3桁ごとの JSON に変換する"""
import csv, json, re, sys
from collections import defaultdict
from pathlib import Path

src = Path(sys.argv[1] if len(sys.argv) > 1 else 'KEN_ALL.CSV')
out = Path(sys.argv[2] if len(sys.argv) > 2 else 'zip')
out.mkdir(exist_ok=True)

rows = []
with src.open(encoding='cp932', newline='') as f:   # Shift_JIS(MS漢字コード)
    for r in csv.reader(f):
        zipcode, pref, city, town = r[2], r[6], r[7], r[8]
        # 町域名が長いと複数行に分割されている:括弧が閉じるまで前の行につなぐ
        if rows and rows[-1]['open'] and rows[-1]['zip'] == zipcode:
            rows[-1]['town'] += town
            rows[-1]['open'] = rows[-1]['town'].count('(') > rows[-1]['town'].count(')')
            continue
        rows.append({'zip': zipcode, 'pref': pref, 'city': city, 'town': town,
                     'open': town.count('(') > town.count(')')})

def clean(town):
    if town == '以下に掲載がない場合':             # 町域を特定しない行
        return ''
    if town.endswith('の次に番地がくる場合') or town.endswith('一円'):
        return ''
    return re.sub(r'(.*)$', '', town)           # 「(次のビルを除く)」などの注記を外す

table = defaultdict(dict)
for r in rows:
    entry = [r['pref'], r['city'], clean(r['town'])]
    lst = table[r['zip'][:3]].setdefault(r['zip'], [])
    if entry not in lst:
        lst.append(entry)

for head, d in table.items():
    (out / f'{head}.json').write_text(json.dumps(d, ensure_ascii=False), encoding='utf-8')
print(f'{len(rows)}件 → {len(table)}ファイル')

CSV と同じ15列・Shift_JIS のサンプル7行(分割された町域を含む)で動かした結果です。

$ python3 ken_all_to_json.py KEN_ALL_sample.CSV zip
6件 → 3ファイル
zip/040.json: {"0400001": [["北海道", "函館市", "サンプル町"]]}
zip/079.json: {"0790177": [["北海道", "美唄市", "上美唄町協和"], ["北海道", "美唄市", "上美唄町南"]]}
zip/100.json: {"1000000": [["東京都", "千代田区", ""]], "1000001": [["東京都", "千代田区", "千代田"]], "1006090": [["東京都", "千代田区", "大手町JAビル"]]}

2行に分かれていた「サンプル町(1~3丁目、」「5番地)」が1件にまとまり、括弧の注記を外した「サンプル町」になっています。「以下に掲載がない場合」の行は町域が空になりました。画面側では fetch('/zip/' + zip.slice(0, 3) + '.json') で読み、data[zip] を候補として使います。

入力欄を作るときのチェックリスト

項目 確認すること
入力の受け付け 全角数字・ハイフンあり/なし・〒付き・貼り付けで動くか
問い合わせの回数 7桁そろうまで呼ばないか。打ち直しで前の結果が入らないか
候補が複数 町域を選べるか(例:079-0177)
該当なし 案内が出て、手入力に進めるか(例:999-9999)
通信エラー・遅延 一定時間で打ち切り、手入力に進めるか
上書き 自動入力のあとで、利用者が町域や番地を直せるか
送信時の検証 自動入力に頼らず、サーバー側でも郵便番号と住所の形式を検証しているか
利用規約 使う API の規約(アクセス回数の制限、保証の範囲)を確認したか

zipcloud の API 利用規約では、運用上必要な場合にアクセス回数やアクセス時間を制限することがあると定められ、提供内容は無保証です。止まっても手入力でフォームが使えるように作っておくと、利用者を待たせずに済みます。

よくある質問

Q. 郵便番号は7桁そろってから検索するべきですか。 A. はい。上3桁や5桁で候補を出す作りもできますが、件数が多くなり、API の呼び出しも増えます。7桁そろってから1回だけ問い合わせるのが一般的です。

Q. 住所の「番地」まで自動で入りますか。 A. 郵便番号で特定できるのは、通常は町域までです。番地・建物名は利用者が入力します。日本郵便のデジタルアドレス(7桁の英数字)を使うと、建物名や部屋番号まで含めて取得できます。

Q. カタカナの読みも入れたいです。 A. zipcloud なら kana1〜kana3(半角カナ)が返ります。全角カナで保存したい場合は、取得後に全角へ変換します。

Q. 大企業や私書箱の個別の郵便番号も引けますか。 A. この記事の CSV(住所の郵便番号)とは別に、日本郵便は事業所の個別郵便番号のデータも公開しています。zipcloud の API が事業所の番号に対応しているかは、利用前に実際の番号で確かめておくと確実です。

Q. API が止まったらどうなりますか。 A. この記事のコードは5秒で打ち切り、「住所を直接入力できます」と案内します。住所欄は手入力できるので、フォーム自体は使えます。

まとめ

  • 7桁そろった時点で1回だけ問い合わせ、結果を都道府県・市区町村・町域に入れるのが基本形
  • input イベント+全角・ハイフンの正規化で、貼り付けや全角入力でも動く
  • 1つの郵便番号に町域が複数あるので、候補を選べる作りにする(例:079-0177 は3件)
  • 該当なし(results: null)・通信エラー・遅延でも、手入力に進めるようにする
  • 方式は zipcloud(無料・登録なし)→ 日本郵便 API(公式・登録あり)→ 自前データ(毎月更新) の順に検討する
  • 自前データは Shift_JIS・「以下に掲載がない場合」・町域の行分割 の3点に気をつけて取り込む

参考・出典

確認日はいずれも 2026年10月10日です。

  • zipcloud「郵便番号検索API」:https://zipcloud.ibsnet.co.jp/doc/api
  • zipcloud「郵便番号検索API 利用規約」:https://zipcloud.ibsnet.co.jp/rule/api
  • 日本郵便「郵便番号・デジタルアドレス API」:https://lp-api.da.pf.japanpost.jp/
  • CodeZine「日本郵便、郵便番号やデジタルアドレスから住所情報を取得できるAPIを無料で提供開始」:https://codezine.jp/news/detail/21592
  • 日本郵便「郵便番号データダウンロード(読み仮名データの促音・拗音を小書きで表記するもの)」:https://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html
  • 日本郵便「郵便番号データの説明」:https://www.post.japanpost.jp/zipcode/dl/readme.html
  • YubinBango(GitHub):https://github.com/yubinbango/yubinbango
  • MDN「AbortController」:https://developer.mozilla.org/ja/docs/Web/API/AbortController
  • MDN「HTML の autocomplete 属性」:https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/autocomplete

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「郵便番号から住所を自動入力|無料APIの比較とJavaScript完成コード」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?