メインコンテンツへスキップ
住所APIナビ

日本の住所入力フォーム設計:都道府県・市区町村・番地・建物名

日本の住所入力フォームを都道府県・市区町村・町域・建物名にどう分けるか、autocomplete 属性の指定、都道府県の入力方式、バリデーションとアクセシビリティの考え方を、動くJSXコード付きで整理します。

情報の最終確認日:ページ更新日:人手による確認待ち

執筆:住所APIナビ編集部

住所入力は項目が多く、入力の負担が大きくなりやすい箇所です。ここでは日本の住所をどの単位で分割し、autocomplete をどう指定し、どこまで検証するかを、実装できる形でまとめます。

動作環境

以下の組み合わせを前提に書いています。掲載しているコードは構文チェックを自動で行っていますが、 断片であるため完全な型チェックは通していません。ご自身の環境で確認してください。

  • Node.js 24.x
  • Next.js 16(App Router)
  • TypeScript 5.9(strict)
  • React 19

郵便番号から住所を引く部分は Next.jsで郵便番号から住所を自動入力する方法 で作った /api/postal-code を呼び出す前提です。まだ用意していない場合も、そのエンドポイントを差し替えればこのフォームはそのまま動きます。

1. 住所欄をどう分割するか

最低限、次の4つに分けます。

自動補完必須
都道府県東京都する必須
市区町村千代田区する必須
町域・番地丸の内1-1-1町域まで必須
建物名・部屋番号〇〇ビル 5Fしない任意

1行の自由入力だけにしない理由は、後工程が困るからです。

  • 配送業者の伝票は都道府県と市区町村を分けて持つことが多く、1行の文字列から機械的に切り出すと「京都府京都市」「広島県広島市」のような重複表記や、政令市の区の扱いで崩れます。
  • 建物名が混ざると、町域の照合が通りません。「〇〇ビル」は住所データベースに存在しないため、実在チェックをかけると弾かれます。
  • 集計や配送エリア判定が都道府県単位でできなくなります。後から文字列を分解し直すことになり、そこで表記ゆれの問題を引き受けることになります。

逆に、分けすぎるのも良くありません。「町域」と「番地」を別欄にすると、郵便番号から町域だけを埋めた後、ユーザーは番地欄にカーソルを移して続きを打つ必要があります。日本の住所は町域と番地の境目が曖昧なケースが多いため、MVP では「町域・番地」を1欄にまとめ、町域までを自動で埋めて、続きをユーザーに打ってもらう形が扱いやすいです。

2. autocomplete 属性を正しく指定する

ブラウザの自動入力は autocomplete のトークンを見て動きます。**APIを叩くより速く、通信も発生しません。**APIを入れる前に、まずここを正しく埋めることをおすすめします。

MDN のトークン定義に従うと、日本の住所欄は次のように対応します。

トークンMDN の定義(要約)
郵便番号postal-code郵便番号
都道府県address-level1住所の第1行政レベル。国によって州・県・カントンなど
市区町村address-level2第2行政レベル。市・町・村などの自治体
町域・番地address-line1街区住所の1行目
建物名・部屋番号address-line2街区住所の2行目
country-name国・地域名

address-level1 は「州・県」に相当するので都道府県address-level2 は「市・町・村」なので市区町村です。この2つを逆にすると、ブラウザが保存している住所と噛み合わず、自動入力が黙って外れます。

name 属性もトークンと揃えておくと、ブラウザによっては推定の精度が上がります。どちらか一方しか書けない場合は autocomplete を優先します。

3. 都道府県は <select><input>

どちらにも欠点があります。

<select> の場合

  • 良い点:入力ミスが構造的に起きない。値が47通りに固定されるので、後段の分岐やバリデーションが単純になる。
  • 悪い点:モバイルでは47件のホイールやリストから探すことになり、スクロール量が多い。「宮城県」と「宮崎県」のような似た名前を選び間違える事故も起きる。

<input> の場合

  • 良い点:キーボードで数文字打てば終わる。ブラウザの自動入力とも相性が良い。
  • 悪い点:「東京」「東京都」「とうきょうと」が混ざる。サーバー側で正規化する処理が必ず必要になる。

折衷案は <input> + <datalist> です。候補は出しつつ、自由入力も許します。

// 候補は出すが、入力自体は自由。値の正規化はサーバー側で行う。
<label htmlFor="prefecture">都道府県</label>
<input
  id="prefecture"
  name="address-level1"
  autoComplete="address-level1"
  list="prefecture-options"
  value={prefecture}
  onChange={(event) => setPrefecture(event.target.value)}
/>
<datalist id="prefecture-options">
  {PREFECTURES.map((name) => (
    <option key={name} value={name} />
  ))}
</datalist>

<datalist> はブラウザによって表示や絞り込みの挙動が異なります。候補が出ない環境でも入力できることが前提なので、サーバー側での正規化は必ず用意してください。

判断の目安としては、配送先として都道府県を厳密に持ちたいなら <select>、入力速度を優先するなら <input> + <datalist> です。この記事のコード例では、値が固定される <select> を採用しています。

47件の一覧は定数として切り出しておきます。

// src/lib/prefectures.ts
 
/** 都道府県コード順(JIS X 0401)。表示順もこの並びを使う。 */
export const PREFECTURES = [
  '北海道',
  '青森県',
  '岩手県',
  '宮城県',
  '秋田県',
  '山形県',
  '福島県',
  '茨城県',
  '栃木県',
  '群馬県',
  '埼玉県',
  '千葉県',
  '東京都',
  '神奈川県',
  '新潟県',
  '富山県',
  '石川県',
  '福井県',
  '山梨県',
  '長野県',
  '岐阜県',
  '静岡県',
  '愛知県',
  '三重県',
  '滋賀県',
  '京都府',
  '大阪府',
  '兵庫県',
  '奈良県',
  '和歌山県',
  '鳥取県',
  '島根県',
  '岡山県',
  '広島県',
  '山口県',
  '徳島県',
  '香川県',
  '愛媛県',
  '高知県',
  '福岡県',
  '佐賀県',
  '長崎県',
  '熊本県',
  '大分県',
  '宮崎県',
  '鹿児島県',
  '沖縄県',
] as const
 
export type Prefecture = (typeof PREFECTURES)[number]

4. 番地・建物名は自動補完しない

郵便番号APIが返すのは町域までです。番地や建物名を返すAPIではありません。

やってはいけないのは、町域を埋めたついでに番地欄にも何かを入れてしまう実装です。ユーザーは埋まっている欄を読み飛ばすため、間違った番地のまま送信されます。空欄なら気づけますが、もっともらしく間違っている値には気づけません。

  • 町域まで埋めたら、カーソルは番地の続きに置くか、番地欄を空のまま残してユーザーに委ねます。
  • 建物名・部屋番号の欄は常に空にします。自動入力の対象にもしません。
  • 補完後に埋まった欄を読み取り専用にしないでください。APIのデータが古い場合、ユーザーが直せなくなります。

5. バリデーションは緩めにする

**厳しすぎる正規表現は、実在する住所を弾きます。**日本の住所には、汎用的なパターンから外れる書き方が普通に存在します。

  • 京都の通り名:「京都市中京区烏丸通御池上る」のように、「通」「上る」「下る」「西入」といった語が入ります。数字とハイフンだけを想定した検証は通りません。
  • 北海道の条丁目:「札幌市中央区北一条西二丁目」のように、町域自体に「条」「丁目」が含まれます。「丁目」を番地側のものと決め打ちすると壊れます。
  • 沖縄の「字」:「中頭郡北谷町字美浜」のように「字(あざ)」「大字」が入ります。「字」を不正文字として弾く実装を時々見かけます。
  • そのほか、「一ノ関」「三ヶ日」のような漢数字・「ノ」「ヶ」の混在、ローマ字表記の建物名、アルファベット入りの部屋番号なども普通にあります。

実務的な落としどころは次のとおりです。

// src/lib/address-validation.ts
 
export type FieldError = { field: string; message: string }
 
const MAX_LENGTHS = {
  addressLine1: 100,
  addressLine2: 100,
} as const
 
/**
 * 住所欄の検証。「実在するか」は判定しない。
 * 弾くのは「空である」「長すぎる」「制御文字が混ざっている」の3つだけ。
 */
export function validateAddress(input: {
  prefecture: string
  city: string
  addressLine1: string
  addressLine2: string
}): FieldError[] {
  const errors: FieldError[] = []
 
  // 制御文字(改行・タブ・NULLなど)はコピペで紛れ込む。表示崩れの原因になる。
  // eslint-disable-next-line no-control-regex
  const hasControlChar = (value: string) => /[\u0000-\u001F\u007F]/u.test(value)
 
  if (input.prefecture.trim() === '') {
    errors.push({
      field: 'prefecture',
      message: '都道府県を選択してください。',
    })
  }
 
  if (input.city.trim() === '') {
    errors.push({ field: 'city', message: '市区町村を入力してください。' })
  }
 
  if (input.addressLine1.trim() === '') {
    errors.push({
      field: 'addressLine1',
      message: '町域・番地を入力してください。',
    })
  } else if (input.addressLine1.length > MAX_LENGTHS.addressLine1) {
    errors.push({
      field: 'addressLine1',
      message: `町域・番地は${MAX_LENGTHS.addressLine1}文字以内で入力してください。`,
    })
  }
 
  // 建物名は任意。入力されたときだけ長さを見る。
  if (input.addressLine2.length > MAX_LENGTHS.addressLine2) {
    errors.push({
      field: 'addressLine2',
      message: `建物名・部屋番号は${MAX_LENGTHS.addressLine2}文字以内で入力してください。`,
    })
  }
 
  for (const [field, value] of Object.entries(input)) {
    if (hasControlChar(value)) {
      errors.push({ field, message: '使用できない文字が含まれています。' })
    }
  }
 
  return errors
}

**「番地が含まれていないようです」は、エラーではなく警告として出します。**送信をブロックしてはいけません。番地のない住所(大字だけで届く地域、私書箱など)は実在します。

全角・半角やハイフンの種類を揃えたい場合は、検証ではなく正規化の仕事です。挙動は住所整形ツールで確認できます。文字列を都道府県・市区町村・町名・番地に分解したい場合は Geolonia normalize-japanese-addresses のようなライブラリが使えます。

6. アクセシビリティ

<label> を必ず関連付ける。 W3C WAI の Labeling Controls は、labelfor 属性が対象コントロールの id と正確に一致する必要があると述べています。プレースホルダは入力が始まると消えるため、ラベルの代わりにはなりません。ラベルを視覚的に隠す場合も、コード上には残します。

エラーは色だけで伝えない。 W3C WAI の User Notifications は、色分けだけに頼らず、ラベルやテキストを併用するよう求めています。赤枠に加えて、必ずテキストのメッセージを出してください。

エラーメッセージは対象の欄に紐づける。 aria-describedby でメッセージ要素を指し、aria-invalid を立てます。メッセージには「何が問題か」と「どう直すか」を書きます。「入力が不正です」だけでは直せません。

非同期の結果は aria-live で通知する。 郵便番号から住所を埋める処理は、画面が勝手に変わる操作です。緊急でない通知なので aria-live="polite"、送信をブロックするエラーには role="alert" を使い分けます。

キーボードだけで完了できるようにする。 補完が終わった瞬間にフォーカスを別の欄へ飛ばすのは避けてください。スクリーンリーダー利用者は、自分がどこにいるか分からなくなります。フォーカスは動かさず、aria-live で「住所を入力しました」と伝えるだけにします。

7. フォーム全体のコード例

Server Component ではなく、通常のクライアントコンポーネントです。入力状態を持つため 'use client' が必要になります。

// src/components/AddressForm.tsx
'use client'
 
import { useId, useState } from 'react'
 
import { PREFECTURES } from '@/lib/prefectures'
 
type Candidate = { prefecture: string; city: string; town: string }
type LookupStatus = 'idle' | 'loading' | 'empty' | 'error'
 
const LOOKUP_TIMEOUT_MS = 5000
 
const STATUS_MESSAGES: Record<LookupStatus, string> = {
  idle: '',
  loading: '住所を検索しています。',
  empty: '該当する住所が見つかりませんでした。都道府県から入力してください。',
  error: '住所を取得できませんでした。そのまま手入力で先に進めます。',
}
 
export function AddressForm() {
  // 同じページに複数フォームが載っても id が衝突しないようにする。
  const uid = useId()
  const [prefecture, setPrefecture] = useState('')
  const [city, setCity] = useState('')
  const [addressLine1, setAddressLine1] = useState('')
  const [status, setStatus] = useState<LookupStatus>('idle')
 
  async function lookup(rawPostalCode: string) {
    // 全角数字とハイフンを落として7桁にそろう場合だけ問い合わせる。
    const digits = rawPostalCode.normalize('NFKC').replace(/[^0-9]/g, '')
    if (digits.length !== 7) {
      return
    }
 
    setStatus('loading')
 
    try {
      const response = await fetch(`/api/postal-code?zipcode=${digits}`, {
        // 応答が返らないまま待ち続けないよう必ず打ち切る。
        signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
      })
 
      if (!response.ok) {
        setStatus('error')
        return
      }
 
      const data = (await response.json()) as { candidates?: Candidate[] }
      const first = data.candidates?.[0]
 
      // 該当なしはエラーではない。手入力に切り替えてもらう。
      if (!first) {
        setStatus('empty')
        return
      }
 
      setPrefecture(first.prefecture)
      setCity(first.city)
      // 町域までしか埋めない。番地の続きはユーザーが打つ。
      setAddressLine1(first.town)
      setStatus('idle')
    } catch {
      // タイムアウト(TimeoutError)も通信断もここに来る。どちらも手入力で回復できる。
      setStatus('error')
    }
  }
 
  return (
    <form action="/checkout/confirm" method="post">
      <div>
        <label htmlFor={`${uid}-postal`}>郵便番号</label>
        <input
          id={`${uid}-postal`}
          name="postal-code"
          type="text"
          inputMode="numeric"
          autoComplete="postal-code"
          maxLength={8}
          aria-describedby={`${uid}-postal-hint`}
          // 1文字ごとに叩かない。欄を離れたときに1回だけ。
          onBlur={(event) => void lookup(event.target.value)}
        />
        <p id={`${uid}-postal-hint`}>
          ハイフンはあってもなくてもかまいません。
        </p>
      </div>
 
      <div>
        <label htmlFor={`${uid}-prefecture`}>都道府県</label>
        <select
          id={`${uid}-prefecture`}
          name="address-level1"
          autoComplete="address-level1"
          required
          value={prefecture}
          onChange={(event) => setPrefecture(event.target.value)}
        >
          <option value="">選択してください</option>
          {PREFECTURES.map((name) => (
            <option key={name} value={name}>
              {name}
            </option>
          ))}
        </select>
      </div>
 
      <div>
        <label htmlFor={`${uid}-city`}>市区町村</label>
        <input
          id={`${uid}-city`}
          name="address-level2"
          autoComplete="address-level2"
          required
          value={city}
          onChange={(event) => setCity(event.target.value)}
        />
      </div>
 
      <div>
        <label htmlFor={`${uid}-line1`}>町域・番地</label>
        <input
          id={`${uid}-line1`}
          name="address-line1"
          autoComplete="address-line1"
          required
          value={addressLine1}
          onChange={(event) => setAddressLine1(event.target.value)}
        />
      </div>
 
      <div>
        {/* 建物名は自動補完しないので state を持つ必要もない。 */}
        <label htmlFor={`${uid}-line2`}>建物名・部屋番号(任意)</label>
        <input
          id={`${uid}-line2`}
          name="address-line2"
          autoComplete="address-line2"
        />
      </div>
 
      {/* 補完の結果は色ではなくテキストで伝え、フォーカスは動かさない。 */}
      <p role="status" aria-live="polite">
        {STATUS_MESSAGES[status]}
      </p>
 
      <button type="submit">確認画面へ</button>
    </form>
  )
}

見落としやすい点

複数候補を1件目で決め打ちしない。 同じ郵便番号に複数の町域が対応することがあります。candidates が2件以上なら選択UIを出します。

補完が失敗してもフォームは止めない。 住所補完は補助機能です。APIが落ちている間も注文を受けられる状態にしておきます。

送信直前に整形しない。 ユーザーの見ている値と送信される値が食い違うと、確認画面の意味がなくなります。整形するなら入力直後か、確認画面で「この内容で送信します」と見せてからにします。

海外住所を扱うなら早めに決める。 都道府県の <select> は日本前提の設計です。国の選択を入れる予定があるなら、住所欄の構造を国ごとに切り替える必要が出てきます。

関連ページ