1. 为什么地名重复问题值得专项治理
先讲一个真实case。去年做一个出行类平台的数据治理,客服转来一条让整个数据组都头疼的投诉:用户想导航去“柳庄村”,结果被带到三十公里外的另一个同名地点,直接误了一桩正事。后台排查了一圈,全省叫“柳庄村”的点位有二十多个,检索接口只是按默认排序给了第一个结果,压根没做任何消歧处理。
这个问题的普遍程度远超普通人的想象。中国的地名体系里,村镇一级的重名率非常高,“张家村”“李家沟”“王家店”几乎是很多省市的标配;老县城里的“解放路”“中山路”“人民广场”也会在不同城市反复出现。对地图产品来说,这既是长期形成的文化现象,更是数据治理中绕不开的硬骨头。
我见过不少团队初期不重视地名重复,等POI数据量涨到几十万、上百万条时才真正意识到问题有多严重:图面上全是重合标注,检索结果一屏刷下来全是同名地点,物流、外卖对接方反馈订单解析错误率飙升。做地图标注优化,如果只盯着字号、颜色、避让策略,而不先把“地名重复”这个根源问题理顺,后面的工作做得再精细,用户看到的依然是一张让人不敢信的地图。
1.1 重复地名的四种典型来源
在真实项目中,地名重复从来不是单一原因造成的。我把它们归纳成四类,处理策略完全不同,分类错了后面全乱:
跨区域同名地物。比如全国有多个“西湖区”“长安镇”“城关镇”,彼此之间没有行政归属关系,但名称完全一致。这一类属于“合法重名”,不需要合并,但必须在展示和检索时做上下文区分。
同一行政区域内的重名点位。同一个县界内出现多个同名村委会、自然村,例如一个县里两个“河东村”,或者三个“张庄”。这类重名大多源于历史上同宗村落拆分,或者取名习惯趋同,也是用户投诉最集中的类型。
同一实体被重复采集。两个数据源先后收录了同一家便利店、同一个加油站,但由于坐标系不同、采集时间不同、录入误差,两个点坐标相差几十米甚至一两百米,名称相同且各自拥有独立ID,于是被当成两个地点展示。这类是真正需要“物理去重”的。
新旧名称并存。因行政区划调整、村庄合并,一部分数据源仍保留旧称,新采集数据用新称,而且新旧名称之间没有建立关联关系,于是“红旗公社”和“红旗乡”同时出现在图面上,看起来像两个不同的地点。
搞清来源之后,才能决定每条记录是走“合并”、走“加前缀”、还是走“标注后缀”的流程。如果一上来就直接按名称做去重,极大概率会把合法的同名地点误删,把数据搞得一团糟。
1.2 重复地名带来的连锁反应
如果只是图面上多几个字,问题还不算严重。真正危险的是它会把下游所有业务链路一起带偏。
对于物流和外卖行业,地址解析依赖地图POI库,系统拿到一个“柳庄村”之后,要在几千个同名候选里选一个作为末端投递点。如果选择的排序规则不合适,包裹就会被派到错误地址。我们当时对接的一家快递平台就反馈过,某个乡镇的两条同名街道导致派送员连续三天跑错门。
城市治理类项目更怕这种情况。网格化管理把每个网格绑定到具体地名和边界,一旦重名数据没有清洗干净,事件上报就会落到错误的网格,后续的派单、处置、责任归属全部错位,每次出问题都要靠人工去反查数据源头。
还有一类影响很隐蔽:统计报表失真。比如某个月新增便利店数量、某个区域公共设施分布密度这类指标,如果同一家门店被重复采集了两次,汇总结果就会整体虚高。数据团队往往最先发现这个问题,但排查成本已经花出去了。
2. 顶层设计:先分清“同名不同地”和“重复录入”
我接触过不少团队处理重复地名的第一反应是:写个脚本,按名称分组,留下一条,删除其余。这种做法看起来效率高,实际上危害很大——它把两类完全不同的情况混为一团。真正要做的是先把“同名不同地”和“同一实体重复录入”分成两条处理线,分别制定规则。
2.1 让每个地名都有完整的“行政上下文”
一个很直观的例子:“凤凰村”这个名字在全国范围可能有上百个。如果表结构里只有一个name字段,那不管是做检索、做渲染还是做去重,都无从下手。所以第一件要做的事,是在数据模型里为每个点位挂上完整的行政区划链条。
我们当时的做法是增加几个固定字段,分别为省份、城市、区县、乡镇/街道、行政村/社区,同时再冗余一个admin_chain字段,直接存储从省级到村级的完整拼接字符串,形如“浙江省/杭州市/余杭区/良渚街道/港南村”。这么设计的目的很直接:处理阶段可以拿到全量上下文,渲染阶段则按需截取展示。
行政区划代码是另一个不能省略的字段。建议采用标准9位或12位区划代码体系,比如杭州市余杭区的代码在旧版本和当前版本可能不同,因此还需要增加一个版本号字段,记录这条地名数据归属的区划版本。否则等某地撤县设区、乡镇合并时,你会发现自己连数据对应的是哪一套区划都分不清。
2.2 判定重复的三要素模型
判断两条记录到底是不是同一个地点,不能靠人工肉眼看,更不能拍脑袋。我们最终确定的是“名称+行政归属+空间距离”三要素模型,缺一个都可能误判:
- 名称:经过标准化处理后的中文字符串。标准化要清洗全角半角、多余空格、繁体简体,但对“村委会”“社区”这类行政后缀要谨慎保留。
- 行政归属:以当前有效行政区划代码为准,而不是以边界多边形为准。
- 空间距离:基于投影坐标或地理坐标计算出来的真实距离,需要明确坐标系和投影方式。
把三要素组合起来,就能得到一套清晰的判定分流表:
| 名称 | 行政归属 | 空间距离 | 判定结论 | 处理方式 |
|---|---|---|---|---|
| 相同 | 相同 | 小于100米 | 同一实体重复采集 | 合并为一条,保留质量更高的记录 |
| 相同 | 相同 | 100米~1公里 | 区域内重名异址 | 加方位词或辅助名,如“凤凰村(东)” |
| 相同 | 相同 | 大于1公里 | 相距较远,疑似真实重名 | 转入人工复核池 |
| 相同 | 不同 | 不限 | 跨区域合法重名 | 展示时拼接行政区前缀 |
这套规则最大的价值在于:它把“玄学”变成了可执行的分流策略。后面所有脚本、SQL、人工复核,都是围绕这张表展开的。
3. 实操过程:一套能直接落地的地名去重流程
下面的流程来自我们当时的项目环境PostgreSQL加PostGIS加Python加QGIS的组合,整套方法可以平移到其他技术栈,关键是思路一致。我按步骤拆开讲。
3.1 第一步:数据标准化
不管原始数据来自自采、外购还是政府公开数据,进入处理流程之前一定要做三件事。
第一,统一坐标系。常见的有WGS84、GCJ-02、BD-09几种,公开地图产品通常使用GCJ-02,如果你手里的底图是WGS84,必须记录转换关系。我当时用Python写了一套批量转换脚本,把历史数据全部转到统一坐标系,再做空间计算。
第二,统一字段结构。至少要包含:名称、完整地址、省、市、区县、乡镇/街道、社区/村、POI类型、经度、纬度、数据来源、采集时间、唯一ID。不少原始数据缺少乡镇级字段,这是后面很多麻烦的根源,宁可处理时多费点功夫也要补齐。
第三,统一名称规范。把全角符号转半角、去掉首尾空格、统一繁体简体,但行政后缀要保留。对于“(临时)”“新址”这类修饰性文字,单独拆到别名或者备注字段,不要直接丢弃。
标准化做完,你的数据才具备被批量比对的基本条件。跳过这一步直接去重,后面大概率会误伤。
3.2 第二步:按名称分组批量检测
标准化之后,先跑第一轮最粗粒度的发现查询。逻辑很简单:按“名称+行政区划代码”分组,统计出现次数,凡是计数大于1的就是疑似重复组。
SELECT name, admin_code, count(*) AS cnt FROM poi_data GROUP BY name, admin_code HAVING count(*) > 1;这一条SQL跑出来的结果往往会让人吓一跳。我当时处理一个约120万条POI的地级市数据,按“名称+区县级行政代码”分组后发现7000多组疑似重复。不要一次性全部处理,先按数量倒序,集中精力看高频重名,例如同一个区内重复出现十几次的那个名字,影响面最大。
建议把结果输出成候选清单,包含点位ID、名称、坐标、来源、采集时间、行政链字段,后面所有人工复核都基于这张表推进。
3.3 第三步:空间邻近度判断
名称相同的候选组并不等于一定需要合并,还要做空间距离计算。用PostGIS可以比较方便地做到:
SELECT a.id AS id_a, b.id AS id_b, a.name, ST_Distance( ST_Transform(a.geom, 3857), ST_Transform(b.geom, 3857) ) AS dist FROM poi_data a JOIN poi_data b ON a.name = b.name AND a.admin_code = b.admin_code AND a.id < b.id WHERE ST_DWithin( ST_Transform(a.geom, 3857), ST_Transform(b.geom, 3857), 1000 );这里的距离阈值1000米可以按业务调整。我们实践中用三档:100米以内视为“疑似同一实体”;100米到1公里之间属于“需加后缀的同名异址”;超过1公里的先不动,转人工复核。
空间计算得到的距离值会有误差,原因是坐标系转换和采集精度都有限制。不要试图用小数点后若干位去较真,只要量级正确就足够做分流了。
3.4 第四步:合并、加后缀、加前缀
拿到三要素判定表之后,按前面设的规则执行:
重复记录处理:保留名称更规范、坐标精度更高、来源更权威的一条记录,另一条标记为“合并来源”,不再参与前端展示,但保留在历史表里。关键是把被合并记录的ID记录下来,后面做下游系统映射要用。
同区域重名异址处理:给次要点位加方位词。例如同一个县有两个“凤凰村”,按照它与乡镇政府驻地的相对方位分别命名为“凤凰村(东)”“凤凰村(西)”,比单纯用经纬度切分南北更符合当地人认知。
跨区域重名处理:展示名称拼接行政区前缀。低层级视角缩放到市级范围时,显示为“杭州西湖区”和“南昌西湖区”,用户一眼就能分辨。
这里还要注意层级联动。如果两个重名点位属于同一个乡镇,前缀只需保留到乡镇级;如果分属不同地市,则要保留到地市级。代码里可以写一个根据当前视图范围动态拼接名称的配置,不要在数据表里把所有前缀都写死。
3.5 第五步:渲染层面的标注优化
数据层处理完,最后落到地图表达上。我们当时用的渲染引擎是Mapbox GL JS,核心思路有三条。
第一,分级控制显示级别。乡镇名在低缩放级别不出现,村级名到zoom 11才显示,避免低级别下一堆同名小字重叠在一起。缩放级别和地名层级的对应关系要在样式里显式配置。
第二,标注优先级。重要地标、交通枢纽、政务机构的地名优先级最高,普通居民区、厂房仓库的优先级低。同一优先级内离视口中心越近越优先显示,其他碰撞的标注自动隐藏。
第三,自动避让。如果同一视口内确实出现多个相同名称,允许渲染引擎根据几何分布自动调整文字锚点位置,尽量避免文字重叠。必要时用换行、竖排等方式做视觉区分。
代码层面就是配置一层地名图层,设置好text-field、min-zoom、max-zoom、symbol-spacing这些参数。核心原则是:展示层只负责“如何好看”,语义层负责“如何不歧义”,两件事分开做,不要全部堆在样式表里。
3.6 第六步:复核、上线与效果验证
自动化流程跑完后,一定要安排人工复核环节。我们当时的做法是从处理结果中随机抽取500个点位,对照遥感影像、实地采集照片和官方地名清单逐一核验,准确率至少达到95%才允许上线。
上线后的验证不能只看数据层。要监控三类指标:搜索点击率是否有提升、同一关键词下的互斥点击比例是否下降、用户投诉量是否减少。如果处理后检索结果依然反复出现歧义,说明前缀规则或优先级配置还有问题,需要回头调整。
4. 实操中的常见坑与排查实录
数据处理这种事,方案看着顺,落地全是坑。我把实际踩过的几个比较有代表性的问题写在这里,希望你能绕开。
4.1 行政区划代码是会过期的
我们当时用了一套固定版本的行政区划代码表,运行半年后某地撤县设区,旧代码大面积失效,新采集的数据无法匹配到正确行政归属,导致一部分重名记录被误判成跨区重名。
这个问题的根源是区划代码不是静态数据。解决办法有两个方向:一是接入标准区划代码的定期更新渠道,每天或每周同步一次;二是在代码表里维护版本区间,标注每条代码的有效起止日期,匹配时优先取当前有效版本。
4.2 边界数据和实际管理范围打架
做空间归属判断的时候,我们最开始直接拿行政边界多边形套点,某一脚踩到别的乡镇边界就自动改归属。结果出现不少离谱情况——同一个村委会的坐标落在邻镇边界内,按边界计算被划到邻镇,实际管理关系却完全不对。
后来改成以行政区划代码字段为准,边界多边形只做参考。两者冲突时优先相信代码字段,再结合自然山河和道路走向做人工确认。记住,行政归属是管理概念,不是纯粹的几何概念,空间计算永远替代不了语义判断。
4.3 名称标准化做过头反而造成误伤
有一版标准化脚本把所有“村委会、X村、社区”全部当作后缀统一删除,想着能提升匹配率。结果“长山村”和“长山镇”因为去掉了后缀全变成了“长山”,被错误判定为同名,差点把两个完全不同的地点合并。
后来我们把标准化收敛为三层:第一层只做全角半角、空格、大小写清洗;第二层做简体繁体统一;第三层只针对明确不会改变语义的修饰词做删除。涉及时空概念的词一律保留,宁可匹配率低一点,也要保证正确率。
4.4 数据清理完,下游系统还在吐旧数据
第一次完成清洗后,我们很开心地看到主库数据干干净净。但没过多久,前端搜索仍然返回了已经被合并的重复POI。查了半天才发现,搜索引擎索引、瓦片缓存、接口层的本地缓存都没有同步更新。地图服务的链路通常包含空间数据库、搜索引擎、缓存服务、瓦片服务等多个系统,只改主库等于没改。
正确的做法是设计一套“数据变更事件”推送机制:主库完成合并操作后,把变更ID列表同步推向搜索引擎和缓存层,触发索引重建和缓存刷新,再通过自动化测试验证下游返回结果里不再包含已被合并的点。
4.5 合并规则写不好,会把好坐标并掉
我们最初设定的合并规则是“保留来源更权威的记录”,结果某个老地名被合并后,新展示的那个点位坐标却明显偏移,因为当地最新采集的那条数据并非来自最权威渠道,反而是一条老数据坐标更准。
后来把规则改成综合加权:坐标精度、采集时间近一年内、被人工核实过、来源可信度这四个维度各占一定权重,取加权得分较高的一条保留。同时保留原坐标字段在历史表中,避免一旦选错无法回溯。
5. 上线之后的持续运维:一次清理只是开始
很多人以为地名去重是一次性项目,处理完就能一劳永逸。实际不是,新采集数据每天都会进入系统,新的重名会持续产生。如果没有一套长期运维机制,半年后问题就会卷土重来。
5.1 建立定期的重名监控任务
我们当时的做法是每周跑一次重名检测脚本,把新增数据放进候选池,自动产出重名监控报表。报表按区域、按重名类型、按影响POI数量三个维度汇总,直接推给数据团队负责人。这样有新问题出现时,不需要等用户投诉就能提前发现。
监控报表还可以加一个趋势对比:这一周的重名数量和上周比是上升还是下降,处理时效如何。有明显上升趋势时,优先排查近期入库的数据源是否发生了变更。
5.2 让用户反馈变成数据修正信号
地图产品一般都有上报纠错入口,用户提交的“地点不存在”“标注错误”“地址重复”反馈,其实是最好的数据修正信号。这些反馈会被我们转到内部工单系统,再由数据清洗流程处理。
处理用户反馈时有一个小技巧:同一地点被多个用户反复反馈,优先级自动提升。如果连续三位用户都指认某个点位有问题,说明它大概率确实有问题,不用再做大规模候选分析,直接进入人工复核队列。
5.3 数据版本管理和操作留痕
凡涉及合并、改名、删除的操作,都必须在系统里留下完整操作日志,包括操作人、操作时间、原记录内容、新记录内容、触发规则。没有日志就没有回滚能力,误操作一旦上线,再想恢复只能靠备份恢复,代价非常高。
我们后来引入了简单的时间戳版本管理:每一次批量处理会生成一个新的数据版本号,保留上一版本快照。如果新版本在质量验证中发现异常,可以一键回滚到旧版本,极为方便。
5.4 与下游业务系统的联动更新
如果地图数据会服务到物流、外卖、零售等外部平台,切记要把“被合并POI的ID映射表”按时同步给它们。因为对接方往往已经把POI ID缓存在自己的数据库里,如果只告诉它们“数据变了”却不给映射关系,它们的历史订单和收藏点位会全部失联。
我们当时发布了一个增量更新包,里面包含三个文件:新增POI列表、变更POI列表、合并ID映射表。下游系统按包更新后,不需要理解内部规则也能保持数据一致性。
最后说一个让我印象很深的细节。上线某天夜里,告警显示一个老地名被自动合并规则改成了另一条坐标更偏的点位。排查后发现,正是前面提到的“合并规则权重失衡”问题导致的。那次之后我彻底明白,地名从来不只是技术数据,它承载着人的记忆和使用习惯。处理地名重复问题时,除了依赖自动化算法,一定要保留人工裁定环节,并在规则设计上给“最新、最准、最常用”三个维度足够的权重。地图是给人用的,只有把数据根子扎稳了,标注优化才有意义。