地址标准化与归一化:同一个地址为什么会有十种写法
五行字符串,一个地点
下面这五行文本指向的是同一个门牌。对人来说一眼就能看出它们是一回事, 但对数据库的等值比较来说,它们是五条毫不相干的记录。
这就是地址标准化要解决的核心问题:把千姿百态的用户输入, 收敛到一个可比较、可去重、可投递的规范形态。 它是地址数据处理里最脏、最琐碎,也最能直接省钱的一环—— 重复的客户档案、失败的投递、对不上的对账,源头往往都在这里。
一、先把四个容易混淆的概念分清
这四个词在中文语境里经常被混用,但它们的输入、输出和依赖完全不同。 搞混的直接后果是:把一个只需要归一化的问题,采购成了一套地理编码服务。
| 概念 | 作用层面 | 做什么 | 依赖 |
|---|---|---|---|
| 归一化 Normalization | 纯文本层面 | 去空白、统一大小写、Unicode 规范化、替换全角半角标点 | 不需要外部数据 |
| 标准化 Standardization | 语义层面 | 把地址拆成元素,再按目标国家的官方规则重新拼装(Street → ST) | 需要该国的缩写表与地址模板 |
| 校验 Validation | 存在性 | 判断这个地址是否真实存在、是否可投递 | 需要权威地址库(如 USPS CASS/DPV、Royal Mail PAF) |
| 地理编码 Geocoding | 空间位置 | 把地址映射为经纬度坐标 | 需要地图数据与匹配算法 |
二、写法差异是从哪来的
缩写自由度
Street 可以写成 St、St.、STREET、Str;Avenue 可以是 Ave、Av、AVENUE。 美国邮政的 Publication 28 给出了官方缩写表(如 ST、AVE、BLVD、PKWY), 但用户并不知道也不会遵守。方位词同理:North 可以是 N、N.、NORTH, 而且它出现在街道名前面还是后面,语义完全不同。
单元标识的混乱
Apt 3B、Apartment 3B、#3B、Unit 3B、No. 3B 表达的是同一件事。 Publication 28 规定单元标识优先放在投递地址行末尾, 并且只在不知道确切标识时才使用井号,且井号与数字之间要留一个空格。 真实数据里当然是各种写法混杂。
行分割的随意性
同样的信息,有人全塞进 address_line1,有人拆到 line2,有人把城市也写进街道行。 跨系统传输时,一个只有单行地址字段的下游会把两行拼起来, 再传回来时就分不清原来的边界在哪里。
语言与转写
同一个日本地址可以写成汉字,也可以写成罗马字,还可能出现长音标记的有无差异; 同一个俄语地址可能是西里尔字母或拉丁转写。 这类差异用纯字符串比较根本无法归并,必须先做转写规范化。
Unicode 层面的隐形差异
带变音符号的字母可以用单个预组合码位表示,也可以用「基字母 + 组合符号」两个码位表示, 肉眼完全一样但字节不同。全角逗号与半角逗号、不间断空格与普通空格同理。 做归一化时对整个字符串执行一次 Unicode 规范化(NFC 或 NFKC)能消掉一大批这类问题。
三、UPU S42:一套描述「地址元素」的通用语言
万国邮联(UPU)的 S42 标准全称是「国际邮政地址组件与模板」, 它做的事情很像一套地址领域的 schema:先定义一份各成员国通用的地址元素清单, 再为每个国家定义模板,说明这些元素在该国应该按什么顺序、什么格式拼装成一行行地址。
数据模型的三个层级
- 元素(element):最小单位,有明确的概念含义,例如「街道号」「道路名称」「道路类型」
- 构造(construct):若干元素或构造的组合,构成地址中一个逻辑片段,例如完整的街道行
- 分段(segment):具有特定功能的一组构造与元素,例如收件人信息段、投递点信息段
举例:「12 Main Street」会被拆成 12(街道号)、Main(道路名称)、Street(道路类型)三个元素。
S42 分为两部分:Part A 定义术语与地址组件(这部分后来成为 ISO 19160-4 的基础), Part B 收录各国的地址模板。一个国家若要加入模板,需要向 S42 专家组提交 二三十条覆盖本国各种地址形态的样例,并把每条样例映射到 S42 元素上; 专家组据此生成模板草案,再用测试程序把元素反向拼回地址,与原始样例比对验证。
对普通开发团队来说,直接实现 S42 通常没有必要,但它的建模思路值得借鉴:把地址存成结构化元素,而不是几行自由文本。 一旦元素是结构化的,标准化就退化成「按模板渲染」, 换一个国家、换一种输出格式(快递面单、发票、地图检索)都只是换模板而已。
四、一条可落地的归一化流水线
下面这条流水线不依赖任何商业服务,能解决绝大部分「同一地址被存成多条」的问题。 关键原则是:原始输入必须原样保留,归一化结果作为额外的派生字段存储。
const STREET_TYPES: Record<string, string> = {
street: "ST", str: "ST", st: "ST",
avenue: "AVE", ave: "AVE", av: "AVE",
boulevard: "BLVD", blvd: "BLVD",
parkway: "PKWY", pkwy: "PKWY", pky: "PKWY",
road: "RD", rd: "RD",
}
const UNIT_DESIGNATORS: Record<string, string> = {
apartment: "APT", apt: "APT", "#": "APT",
suite: "STE", ste: "STE",
unit: "UNIT", building: "BLDG", floor: "FL", room: "RM",
}
export function normalizeUsLine(raw: string): string {
return raw
.normalize("NFKC") // 1. Unicode 规范化,消除全角/组合字符差异
.toUpperCase() // 2. 统一大小写
.replace(/[.,]/g, " ") // 3. 去掉句点与逗号
.replace(/\s+/g, " ") // 4. 折叠连续空白
.trim()
.split(" ") // 5. 逐词映射到官方缩写
.map((w) => {
const k = w.toLowerCase()
return STREET_TYPES[k] ?? UNIT_DESIGNATORS[k] ?? w
})
.join(" ")
}
// 6. 用归一化结果生成匹配键,用于去重与幂等写入
export function addressKey(a: Address): string {
return [a.country, a.postalCode?.replace(/\W/g, ""),
normalizeUsLine(a.line1), normalizeUsLine(a.line2 ?? "")]
.filter(Boolean).join("|")
}该做的
- • 原始输入与归一化结果分列存储,永不覆盖
- • 归一化规则按国家分开,缩写表不要跨国复用
- • 匹配键随规则版本变化,需要能全量重算
- • 对匹配键建索引,去重与幂等写入都靠它
不该做的
- • 把归一化后的大写形态直接展示给用户
- • 在归一化时顺手「猜」并修正用户的门牌号
- • 用编辑距离做全局模糊匹配(会把隔壁门牌合并掉)
- • 把地址第二行也塞进匹配键做严格等值(单元号缺失是常态)
五、去重时的一个重要陷阱
很多团队会用字符串相似度(编辑距离、Jaccard 等)来判断两个地址是不是同一个。 这在街道名的拼写纠错上有效,但用在门牌号和单元号上极其危险: 「123 Main St」和「125 Main St」的相似度非常高,却是两户不同的人家; 「APT 3B」和「APT 8B」只差一个字符,可能差着五层楼。
稳妥的做法是分字段区别对待:门牌号、单元号、邮编必须严格相等;街道名、城市名允许模糊匹配; 两者都命中才判定为同一地址。相似但不完全相同的候选, 交给人工复核或降级为「疑似重复」标记,而不是自动合并。
小结
地址标准化的目标不是让数据「好看」,而是让「同一个地点」在系统里有唯一且稳定的表示。 从 Unicode 规范化和缩写映射这两件低成本的事情做起, 再按需引入官方地址库做存在性校验,收益曲线会非常陡。 要验证归一化规则是否覆盖了各国的写法差异, 可以用本站的地址生成工具批量产出多国样例,作为回归测试的输入。