邮政编码看起来很简单——五位数字,有时再加四位——但这一系统比看上去更有结构,而把校验与地理映射规则搞错会引发真实 bug。本指南涵盖美国邮政编码的结构、第一位数字如何编码大致地理区域、ZIP+4 如何工作、JavaScript/Python/正则中最实用的校验模式,以及你的代码应当优雅处理的边界情况。

简史

Zone Improvement Plan(ZIP)由美国邮政署(USPS)于 1963 年推出,以加快邮件分拣。原始的 5 位编码定义了一个投递区。1983 年新增的 ZIP+4 用于标识一个街区或一个高业务量的收件单位(如企业或政府机关)。大多数面向用户的代码只需 5 位版本,但完全忽略 ZIP+4 是最常见的校验 bug。

5 位邮编的结构

标准邮编的五位数字并不是随机的。它们是一段层级化的地址:

  • 第 1 位——国家级区域:九大宽广地理区域之一。东北从 0 起,西海岸以 9 收尾。
  • 第 2–3 位——分区中心设施(SCF):服务于一批邮局的处理中心。
  • 第 4–5 位——投递区:SCF 内某个具体邮局或投递单位。

仅凭第一位就能判断大致区域。前三位(“邮编前缀”)能可靠地映射到一个州——这正是我们的 邮编查询工具用来识别邮编所属州的依据。

第 1 位对应的区域

第一位区域示例州
0新英格兰与波多黎各 / 美属维尔京CT, MA, ME, NH, NJ, RI, VT, PR
1中大西洋地区DE, NY, PA, 华盛顿特区
2东南 / 阿巴拉契亚NC, SC, VA, WV, MD, DC
3东南AL, FL, GA, MS, TN, KY
4中西部 / 五大湖OH, IN, MI, WI, IL
5上中西部 / 平原IA, MN, MT, ND, SD, NE, KS, MO, WI
6中南部AR, LA, OK, TX, NM
7南部 / 中南部OK, TX, AR, LA
8山地西部AZ, CO, ID, NM, NV, UT, WY
9西海岸及太平洋属地CA, OR, WA, AK, HI, GU, AS

注意,有些州的邮编前缀会横跨区域边界——比如肯塔基既有 4 开头也有其他。“这个邮编是否属于某州”的可靠信号仍是前三位,而不是仅看第一位。

ZIP+4——多出的四位意味着什么

ZIP+4 在 5 位邮编后加一个连字符再加四位数字:

  • 多 2 位:具体某个城市街区或一组街道
  • 多 2 位:该街区的具体某段,或某个大型收件单位

大多数现代地址表单同时接受 5 位或完整的 ZIP+4。USPS 更偏好 ZIP+4,因为这让自动化分拣机可以直接把邮件投到投递员路线;但传统的 5 位邮编仍然普遍有效。

用正则校验

最简单稳健的模式允许 5 位数字,可选地后接一个连字符和 4 位数字:

^\d{5}(?:-\d{4})?$

该模式接受 9021090210-1234,同时拒绝空格、填充和含奇怪字符的邮编。一个常见错误是强制要求 ZIP+4,这会破坏每一个被 Chrome 保存的地址自动填充的注册表单。

用 JavaScript 校验

const ZIP_RE = /^\d{5}(?:-\d{4})?$/;

function normalizeZip(input) {
  const zip = String(input).trim();
  if (!ZIP_RE.test(zip)) return null;
  return zip.length === 10 ? zip : zip.slice(0, 5);
}

normalizeZip("  90210-1234 "); // "90210-1234"
normalizeZip("90210");        // "90210"
normalizeZip("9021");         // null
normalizeZip("90210 1234");   // null(空格不被允许)

normalizeZip 辅助函数要么返回规范格式,要么返回 null,让你在 UI 中分流转很方便:返回 null 时显示行内错误;否则把规范值传给你的 API。

用 Python 校验

import re

ZIP_RE = re.compile(r"^\d{5}(?:-\d{4})?$")

def normalize_zip(value: str) -> str | None:
    zip_code = value.strip()
    if not ZIP_RE.match(zip_code):
        return None
    return zip_code[:5] if len(zip_code) == 5 else zip_code

若你需要一次性完成校验与地理编码,USA Data Tools 地址 API 会为任意格式合法的邮编返回对应州,让你无需自带前缀表就能拒绝不匹配任何州的邮编。

把邮编映射到州

前三位映射到一个 USPS SCF,而该 SCF 又映射到一个或多个州。实用的查找表相当大,你不该手维护——要么用 邮编查询工具,要么用地址 API:

async function stateForZip(zip) {
  const r = await fetch(
    `/us-address/api/v1/zip-lookup?zip=${encodeURIComponent(zip)}`,
    { headers: { Authorization: `Bearer ${KEY}` } }
  );
  if (!r.ok) return null;
  const { state } = await r.json();
  return state;
}

值得了解的边界情况:少数邮编确实存在歧义——例如联邦联合摄影邮编、以及跨州边界的大型企业邮编。在表单设计中,最好同时索取邮编州,再校验二者一致。这样能同时抓住拼写错误和罕见的歧义情况。

军事与属地邮编

你的校验器应当接受以下这些看起来不同寻常的邮编:

  • 美国军方 APO/FPO/DPO:340xx 区间的邮编,州缩写为 AAAEAP。这些是经 USPS 投递的海外军方地址。
  • 波多黎各:006xx–009xx 区间,州缩写 PR。有时仅接受 PR 而非拼写全称“Puerto Rico”。
  • 美属维尔京群岛:008xx,州 VI
  • 关岛、美属萨摩亚、北马里亚纳:969xx 区间,州 GUASMP

拒收这些邮编的配送逻辑是电商中最常见也最容易修的 bug 之一。

常见校验错误

  1. 未去除首尾空白。移动端自动填充经常在前或后加空格——校验前先 trim。
  2. 用整数类型存储。把邮编存成整数会丢失前导 0,导致每个新罕布什尔(030xx)和波多黎各(006xx)邮编都被破坏。邮编永远是字符串。
  3. 强制 ZIP+4。在注册表单上要求扩展码是徒增摩擦收益甚微。
  4. 不拒绝伪造的测试邮编,如 0000012345。某些邮编是真实的(12345 是纽约斯克内克塔迪通用电气的邮编),有些看似有效却未分配——用查找表或 API 拒绝未分配的。
  5. 只按第 1 位路由而不是前缀。第 1 位无法唯一标识一个州。

提示:生成假地址时,请确保邮编与州的配对内部一致。随机 5 位数字配一个随机州会在你自己的下游校验里失败。我们的地址生成器从按州维护的前缀列表中选取邮编,这就是为什么生成的“加利福尼亚”地址总是带 90xxx–96xxx 邮编。

代码评审可直接粘贴的速查表

// 接受:5 位数字,可选 ZIP+4
const ZIP_RE = /^\d{5}(?:-\d{4})?$/;

// 绝不要:把邮编存成整数
// 绝不要:input 的 maxlength=5(会破坏 ZIP+4 的粘贴)
// 绝不要:在公开表单强制 ZIP+4
// 一定要:trim 空白、按字符串存储、按州校验

总结

邮政编码是一小块规则却微妙的数据。对大多数应用,5 位版本就够了;ZIP+4 是锦上添花。前三位是能可靠映射到州的最小前缀。先在本地校验格式,并在信任该值进入下游前——最好用一张维护中的查找表或 USA Data Tools API——校验邮编与州的对应关系。把这些做好,你的地址表单就能少出边界类 bug。