把脏地址解析成区划码(四):把交付件也拉去考试

0 阅读7分钟

前三篇讲的都是"引擎答得对不对"。但客户拿到的不是引擎,是一份 CSV 和一批码。

所以我又做了一件事:造 2000 行"像真实客户导出"的脏地址,三列客户编号 / 地址 / 备注,覆盖 16 种形态(完整五级、缺省、缺市、只有县名、同音错字、旧名、全角括号、空格分隔、邮编噪声、写到村级、占位符、大区泛称、地标、合称、重复省名、开发区),端到端跑一遍交付脚本,看交付件本身会不会出问题。

2000 行 3.9 秒跑完,大约 516 行/秒,人工介入 0 次。这一轮又逼出 4 个评测集永远测不到的问题。

第 16 关:6 位邮编被当成了区划码

邮编341226四川省理塘县,解析成了安徽阜阳颍上县。

因为 341226 恰好是颍上县的区划码,而入口逻辑是"文本里出现 6 位数字就优先按编码解读"。问题是邮编也是 6 位,这个长度是撞的。

改了两条。紧跟"邮"字的数字明确跳过;文本本身能解析到县级时以地址为准,只有地址解析不出东西时才回退去解读编码。510124是哪里 这种纯编码查询照旧。

第 17 关:只答到省,就以为答完了

云南省五定县 只返回了"云南省"。

"五定"是"武定"的同音错字,本该能纠。但原来纠错只在完全没解析出来、或者结果是歧义时才触发,而这里"成功解析到省级"被当成了完成态,纠错根本没机会跑。

改法是,把"只到省级、或者只给出特殊口径但没有区县码"也算作浅解析,照样触发纠错。但只接受层级更深的结果,省级以外还得真往下走一层才改口,避免把本来正确的省级答案纠坏。

第 18 关:市名不看位置

广东省揭阳市榕城区中山路97号,解析成了广东省中山市。

"中山路"里有"中山",命中了中山市,而它比句首的揭阳市先被遍历到,就赢了。

改法:地级市候选按出现位置取最早,全名命中优先于短形式。

同一批还有个更隐蔽的输入,峨眉山市 变成了四川省眉山市。因为"眉山市"是"峨眉山市"的子串,一个纯子串判断把县级市认成了地级市,还丢了区县码。改成边界感知匹配,并且要求匹配窗口的前一个字不能把名字"扩展"成更长的已知名。

第 19 关:别名替换越界

陕西省商洛市镇安县,被替换成"陕西省商洛市镇"加上四川绵阳安州区。因为别名表里有"安县"(四川安县 2016 年改安州区),它在"镇安县"里被命中了。

边界判断本该拦住它,为什么没拦住?查下去发现,边界判断里有一条"前缀是不是以已知区划名结尾",而那个名字集包含 3.8 万个乡镇名,而"洛市镇"恰好是江西丰城的一个镇,跨着"商洛市镇"这个词尾命中,就把该位置判成了合法边界。

改法是让边界判断只用省 / 市 / 县三级名字。乡镇名太多太短,跨词巧合的概率太高。

同一批还有两处同类问题。别名键是更长全名的前缀时不再替换,否则"广西"会在"广西壮族自治区"里再替换一次,拼成"广西壮族自治区壮族自治区";拼音纠错跳过别名词典里的合法写法,否则"新疆维吾尔自治区"里的"新疆"会被当成错字,纠成山西新绛县。

顺带一关:事件库里 623 条"假更名"

这关不是引擎的错,是数据的错,而且藏得很深。

阿城县 一直解析成"黑龙江省-大庆市-杜尔伯特蒙古族自治县"。查下去,发现事件库里有这样一对记录:

1982 更名  海伦县(232121) → 阿城县(232121)
1982 更名  阿城县(232226) → 杜尔伯特蒙古族自治县(232226)

同一个码在不同年份指向了不同的县。这不是更名,是码位改派,也就是同一个码被分配给了另一个县。我的事件库是拿 GB/T 2260 逐年快照做集合差分生成的,差分看不出"码被改派",就把它当成了更名。这样的假记录一共 623 条,其中 26 对是 A→B 与 B→A 互指的互换码,并且已经污染出错误别名,比如 芳村区 → 广州市天河区

改法选的是改标签,而不是删记录。这些记录事实层面是真的,该码当年确实从 A 转到了 B,只是类型不该叫"更名"。改判为"码位改派"之后,引擎不再据此生成别名,事件总量 5,276 条不变,自动生成的历史别名从 915 条降到 843 条(事件库后续扩充至 5,276 条,派生别名同步增至 1,214 条),挤掉了 72 条错误别名。阿城县 也回到正确的哈尔滨市阿城区。

判据是:真更名意味着新名首次出现,所以"新名在该事件前一年的快照里已经存在"就是伪更名。用这个判据,市中区双桥区 这类"同名存在于别的城市"的真更名不会被误伤。


现在的账

数(截至 2026-09-18)
本篇这 4 关各补 3 条以上回归,共 19 条
累计登记修复(台账)43 条
累计新增回归用例138 条,回归集从 110 条扩到 259 条,全绿
四轮评测集128 → 295 → 1,170 条
交付演练2,000 行 / 4–10 秒 / 人工介入 0 次 / 复核率 12.7–13.0% / 判错 0 条
自动生成的历史别名1,214 条,已剔除伪记录
旧码到现行码的映射2,910 条

本系列写作期间引擎持续迭代:上表为发布时的当前值;前几篇的表格是各自阶段的快照,数字小于此处。

评测集(对抗集加回归集)都是我们自己出的题,用途是压缺陷和防回归,不作为准确率承诺。

关于那个"48 小时"

我把交付能力说成"≤48 小时",这里讲清楚它的含义。机器解析是秒级的,2000 行 3.9 秒,48 小时约束的是批量排期加上人工复核。真正决定交付工时的是复核率 13%,也就是有多少行需要人眼过一遍。

这也是为什么每行输出都带状态、复核标记和匹配依据。把人工时间花在刀刃上,比把准确率数字做漂亮更省钱。

我明确不做的那些事

一个诚实的能力清单,比一个漂亮的准确率数字有用得多。

  • 近音、多音字不纠:重亲 到重庆、城都 到成都,一律拒绝。只纠同音,对应拼音输入法的错误形态。近音纠错会撞上大量真实地名,误伤率大于收益。
  • 大区、方位泛称不猜:华北苏北长三角 如实拒绝。它们没有行政区划定义,猜一个省等于编数据。
  • 同名不给唯一答案:同名区县一律输出候选集,不赌。
  • 开发区不编码:苏州工业园区 落到苏州市并标注"非正式建制",绝不虚构一个区划码。
  • 龙潭街道 这种只给街道名、且以某区县短名开头的,按前缀归到该区县。这是取舍,不是遗漏。

这些边界都写在仓库文档里。

开源

引擎和数据在 GitHub:github.com/nbsetsail/r…

代码 MIT,数据 CC-BY-4.0,署名即可使用。四级带官方 12 位码,村级 604,626 条,变更事件库带出处,脏地址解析可离线跑,有 CLI。

如果你手上有被解析错、而你又确信正确答案的地址,请告诉我。