资讯动态

POI地名重复分析与去重:从排查到治理的完整解析

发布时间:2026/9/26 17:02:01 来源:尧图企业网站定制
上个月处理一批城区POI数据光一个“万达广场”在同个街道办事处范围里就查出7条记录坐标偏差最近的只有十几米。更要命的是这7条还分别挂着“万达广场”“万达广场购物中心店”“XX万达广场A座”三个不同写法。这种问题在地图数据圈里太熟悉了——地名重复。同一个地理实体被标注成多条记录或者不同地理实体共享同一个名字轻则搜索结果让人点错重则导航路线直接带偏。这篇文章把地图标注里的地名重复问题拆开讲一遍它到底怎么产生的、怎么批量排查、有哪些去重策略以及一套我实际跑通了的处理流程。适合做地图数据质检、GIS数据处理、POI运营的人也适合刚入行、被同名POI搞得头疼的标注团队。你不需要提前懂很多跟着流程走一遍就能上手。1. 地图标注中的地名重复到底是什么问题1.1 三种典型的重复形态我在实际工作中习惯把地名重复分成三类因为它们的成因、危害和治理手段完全不一样。第一类是跨区域同名。比如“人民路”“中山路”“建设路”这种几乎每个城市都有一条甚至一个城市的不同城区里还有好几条。这类重复本质上是真实世界的合法重名不是数据录错了。它的危害主要体现在全局搜索用户搜“人民路”出来一长串结果不放大地图根本不知道哪条是自己要找的。处理这类问题重点不在“删”而在“区分”——给每条路加上行政区划上下文。第二类是同区域近距同名。同一个小区门口有两家“欣欣超市”同一条商业街上出现两个“创新大厦”这种是最让人头疼的。它们物理距离近、名称完全一致导航规划路径时很容易指错。这类重复绝大多数是数据生产环节造成的不同来源的数据交叉采集同一个实体被重复标了多次坐标偏差只有几十米名称字段又不完全统一。第三类是语义近似同名。同一个实体一个数据源录的是“国际商贸中心”另一个录的是“国际商务中心”一个是“XX大厦”另一个是“XX大厦B座”。这类最阴险因为用精确匹配SQL完全查不出来必须靠模糊匹配、拼音归一化和人工抽检才能捞出来。三种形态的对比我整理成了下面这张表重复类型典型表现主要症结处理重点跨区域同名多条“人民路”分布在多个城区实体本身合法重名数据无错加前缀、区分上下文同区域近距同名同一商圈两条“创新大厦”多源采集交叉录入数据冗余合并、改名、保留唯一实体语义近似同名同一大厦被写成不同写法名称标准化缺失人工录入随意归一化、别名映射、模糊匹配1.2 重复地名的根因不只是标注员手滑很多人觉得地名重复就是标注员责任心不够实际原因要比这个复杂得多。第一个根因是多源数据融合。一套POI库往往来自测绘院、运营商基站数据、第三方数据服务商、UGC上报等多个渠道。同一个实体测绘院采样一个点运营商标注另一个点网友又上报第三个位置三个坐标都落在合理误差范围内名称写法却各自为政。合并的时候如果缺少去重环节重复记录就自然沉淀下来了。第二个根因是人工录入的随意性。标注团队在录入时没有强制规则有人写“万达广场”有人写“万达广场购物中心店”还有人把“XX路XX号”一起塞进名称字段。同一套数据里名称字段的“语义颗粒度”完全不一致这给后面对照去重制造了巨大障碍。第三个根因是历史沿革与行政区划调整。乡镇合并、撤县设区后老地名和新地名在一段时间内会并存。数据更新如果只做增量不做同步新老名称就会被当成两个实体长期留在库里。这类重复往往还牵扯到别名问题处理的时候不能一刀切删除。第四个根因最容易被忽略POI缺少全局唯一标识。很多系统的主键就是自增ID同一实体在不同批次导入时各拿各的ID没有业务层面的唯一键兜底。坐标稍微偏一点、名称稍微变一下就生成新记录这是重复数据的“生产线”。2. 从头排查把重复地名从数据里“捞”出来2.1 空间维度用坐标近邻聚类找“贴脸同名”排查重复的第一步不是比名字而是先看空间位置。原因很简单真正需要警惕的同名风险大多数发生在距离很近的范围内。两个“朝阳宾馆”一个在北京一个在广州大概率是各自独立的真实实体名字只是巧合但如果在同一个路口就有两家那就要重点核实是不是同一个。我用PostGIS做空间近邻检测比较多。核心思路是给POI表建GIST空间索引然后两两对比找出距离小于某个阈值且名称完全相同的记录对。SQL写出来大概是这个感觉-- 给空间字段建索引数据量大时这一步必不可少 CREATE INDEX idx_poi_geom ON poi USING GIST(geom); -- 找出500米内名称相同的POI对 SELECT a.poi_id AS a_id, b.poi_id AS b_id, a.name, ST_Distance(a.geom, b.geom) AS dist_m FROM poi a JOIN poi b ON ST_DWithin(a.geom, b.geom, 500) AND a.name b.name AND a.poi_id b.poi_id WHERE a.category b.category ORDER BY a.name, dist_m;这里有几个细节要注意。a.poi_id b.poi_id能把双向对比砍成单向避免同一条记录对出现两次。ST_DWithin比先算ST_Distance再拿数值比较要高效得多因为它能直接利用空间索引做过滤。阈值500米不是固定值我在主城区一般用300米郊区或产业园区放宽到1000米。因为不同场景下坐标采样精度不同市中心道路密集POI密度高阈值设太大误报率会暴涨。如果你手头没有PostGIS用Python自己算也没问题。重点是先按名称聚合再在同一组里做两两距离计算避免全表笛卡尔积。朴素的O(n²)循环在几十万条数据下会很慢按名称分组之后计算量会小几个量级。2.2 语义维度名称归一化与模糊匹配空间近邻能抓出“名称完全相同”的重复但抓不出“写法不同但实际是同一个”的记录。这时候就要做语义维度的排查。我管这一套动作叫“名称归一化”。核心是把乱七八糟的写法洗成同一套表达再去做匹配。实际操作时我至少会做这几步全角转半角去掉所有不可见字符括号统一成半角括号去掉括号内与标识无关的冗余词去掉名称首尾的通用后缀词比如“店铺”“商铺”“店”等统一大小写对英文品牌名尤其重要洗完之后精确匹配的SQL就可以上场了SELECT normalized_name, COUNT(*) AS cnt, array_agg(poi_id) AS poi_ids FROM ( SELECT poi_id, normalize_name(name) AS normalized_name FROM poi ) t GROUP BY normalized_name HAVING COUNT(*) 1 ORDER BY cnt DESC;但精确匹配只是基建真正的重点是模糊匹配。中文里的近似同名比如“国际商贸中心”和“国际商务中心”编辑距离只有一个字但含义上可能是同一个项目的不同叫法。我常用的工具是PostgreSQL的pg_trgm模块它对中文分词后的相似度计算效果还不错。核心SQL是这样的CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX idx_poi_name_trgm ON poi USING GIN (name gin_trgm_ops); SELECT a.poi_id AS a_id, b.poi_id AS b_id, a.name AS name_a, b.name AS name_b, similarity(a.name, b.name) AS sim FROM poi a JOIN poi b ON similarity(a.name, b.name) 0.8 AND a.poi_id b.poi_id AND a.category b.category ORDER BY sim DESC;相似度阈值0.8是个经验值太低会引入大量无关结果太高会漏掉真重复。我在实际项目中通常从0.7开始试往下捞候选集再用人工复核筛。2.3 综合打分谁留下、谁改名、谁删除把空间和语义两轮结果合并之后你会得到一张“疑似重复候选表”。接下来最忌讳的事情就是拍脑袋决定看这个顺眼就留这个看那个不顺眼就删那个。这种主观操作后续特别容易翻车而且复核回溯时完全说不清当时为什么这么处理。我习惯引入一套打分逻辑。对每一组候选记录从几个维度分别打分最后按总分排序。分数高的作为主记录保留分数低的作为待合并或待删除记录处理。维度打分规则数据源可信度测绘/官方来源3分行业数据商2分UGC/众包1分信息完整度地址、电话、营业时间等字段齐全2分缺失字段倒扣坐标精度采样精度高偏移小1分粗采或明显漂移0分更新状态最近90天内有更新2分长期无更新不加分热度通过搜索点击或访问频次给热度加成仅用于并列时区分打分的意义不在于追求绝对客观而在于让每个处理决定都有据可查。我见过不少项目前期去重靠感觉后期审图委外检查时一问三不知只能重新翻数据成本翻了好几倍。3. 去重策略怎么选改名、加前缀还是建别名3.1 显示层的临时方案加方位或功能限定词数据排查完之后真正落到地图上的处理策略需要分场景设计。一个比较常见的方案是“加方位或功能限定词”。举个例子城东和城西各有一个“花园小区”直接改造成“东花园小区”和“西花园小区”这是最自然的处理方式。类似地两家“滨江大酒店”可以借鉴酒店行业的常用做法一家叫“滨江大酒店滨江路店”另一家叫“滨江大酒店会展中心店”。这种加后缀的方法在显示层没有歧义用户一眼就能分清楚。但这里有个坑改名会破坏用户的既有认知。用户习惯搜“花园小区”你把他常去那个改成“东花园小区”他搜原来的名字可能就找不到了。所以凡是做了显示名改动的记录几乎必然要配套建立别名映射。改完名不等于完事改完名还得“留后门”。3.2 行政区划前缀把“全名”和“显示名”分开对于跨区域同名问题比如多个城区都有“人民路”最稳妥的处理是加行政区划前缀。但这里我不建议直接把前缀硬拼进名称字段因为“XX区人民路”这种字符串一旦落到地图上会特别长小比例尺显示时还会遮挡其他要素。更合理的做法是把行政区划作为独立的属性存起来让“名称”和“显示规则”解耦。数据结构上一个POI应该同时持有主名称字段存“人民路”这种最核心的专名行政区划字段存所属区/街道的编码和名称完整名称可以由“区划主名”动态拼出来地图渲染时大比例尺只显示“人民路”小比例尺显示“XX区·人民路”搜索索引里存完整组合。这样既解决了区分问题又不会污染地图画面。这个思路本质上就是“显示名与逻辑名分离”。3.3 别名体系给“俗称”一个合法入口在去重的整个链条里别名体系是最容易被漏掉、却最影响用户体验的一环。一个真实的POI往往不止一个叫法。本地人管“第一人民医院”叫“市一医院”管“中央商务区”叫“CBD”。如果数据表里只有一个主名用户搜“市一医院”就啥也搜不到或者被错误地召回另一个城市同名医院。更麻烦的是同一个实体一旦有多个叫法不同数据源各录各的下一轮数据融合时又会重新生成“重复记录”。我给POI建模时一般会专门加一张别名表poi_id | alias_name | alias_type -------------------------------- 1001 | 市一医院 | 俗称 1001 | 第一人民医院 | 主名 1002 | 会展中心 | 简称 1002 | 国际会议展览中心 | 正式名搜索时任意别名都能命中唯一的poi_id但展示时只显示主名。别名体系还有一个好处当两个同名POI因为历史原因保留同样主名时可以靠各自别名区分——一个叫“滨江大酒店滨江路店”另一个叫“滨江大酒店会展中心店”用户搜“滨江大酒店”时两个结果都出现但各自都带限定后缀不会混淆。3.4 分层数据模型用唯一ID根治重复如果项目时间允许我强烈建议把POI数据模型从“一条记录存所有信息”升级成“分层模型”。我实际踩坑之后得出的结论是地名重复不是一个一次性的清洗任务它是一种会持续发生的数据状态。只要数据还维持着“一条记录所有字段都塞一起”的结构下次再有数据源汇入重复问题一定会复发。分层模型的基本思想是把POI拆成四层实体层负责存唯一ID、空间坐标、主名称一个实体只有一条记录行政区划层负责存实体与省市区街道的所属关系检索层负责存储主名、别名、拼音、同义词、历史曾用名显示层负责存储渲染优先级、缩放级别、标注避让规则这种模型的优势显而易见修改名称只改实体层新增别称只加检索层行政区划调整只更新区划层。每一层独立变化不会牵一发动全身。代价是建表和查询复杂度变高需要应用层多做一次关联查询。但从长线维护角度看这套结构的收益远比省下的那点开发量值钱。4. 实操全流程从批量清洗到人工复核4.1 数据标准化先把脏字符串洗成同一套语言正式开始排查去重之前一定要先做数据标准化。这一步不做后面的模糊匹配全白搭。标准化至少覆盖四个字段名称、地址、类别、坐标。名称字段要做格式清洗地址字段要拆分出道路和门牌类别字段要统一到同一套分类体系坐标字段要检查经纬度是否反了、是否偏离行政边界。我自己的做法是先跑一遍Python清洗函数把名称字段弄“干净”。一个精简版本大概是这样的import re def normalize_name(raw): if not isinstance(raw, str): return text raw.strip() # 统一括号 text text.replace(, ().replace(, )) # 去除空白字符 text re.sub(r\s, , text) # 统一英文大小写 text text.lower() # 去掉冗余后缀注意这里不能乱删 text re.sub(r(店铺|分店|门店)$, , text) return text注意最后一步的删除后缀要非常克制。像“XX路”的“路”是通名不能删“XX有限公司”的“公司”也不建议删因为“有限公司”和“有限责任公司”在模糊匹配时本身就有区分价值。我见过有人写清洗规则把“大厦”也删掉结果“科技大厦”和“科技园”混成一个误报率直接翻倍。4.2 自动排查脚本一节课能跑完的Python方案数据清洗完就可以执行自动排查了。我用Python写过一套比较顺手的流程读CSV、按名称分组、在同组内做两两距离计算、输出疑似重复列表。下面这段是核心逻辑可以直接抄去改改用import pandas as pd import math def haversine(lon1, lat1, lon2, lat2): R 6371000 p1 math.radians(lat1) p2 math.radians(lat2) dp math.radians(lat2 - lat1) dl math.radians(lon2 - lon1) a (math.sin(dp / 2) ** 2 math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2) return 2 * R * math.asin(math.sqrt(a)) df pd.read_csv(poi_export.csv) df[name_clean] df[name].apply(normalize_name) # 按清洗后的名称分组 groups df.groupby(name_clean) candidates [] for name, group in groups: if len(group) 2: continue recs group.to_dict(records) # 组内两两比较距离小于阈值的标记为候选 for i in range(len(recs)): for j in range(i 1, len(recs)): dist haversine(recs[i][lng], recs[i][lat], recs[j][lng], recs[j][lat]) if dist 500: candidates.append({ name: name, poi_id_a: recs[i][poi_id], poi_id_b: recs[j][poi_id], dist_m: round(dist, 1), name_a: recs[i][name], name_b: recs[j][name], score_a: recs[i][score], score_b: recs[j][score] }) cand_df pd.DataFrame(candidates) cand_df.to_csv(candidates_500m.csv, indexFalse)这段脚本有几个点值得说。按清洗后的名称分组可以极大压缩两两比较的规模。用了Haversine公式而不是geopy因为它在无外部依赖的时候也能跑几十万条数据量下性能可以接受。最后输出里带上score_a和score_b两个打分字段目的就是让候选表天生自带“该留谁”的判断线索。4.3 人工复核与合并台账自动排查只能产出“疑似重复清单”真正的判断必须由人来做。我见过很多团队想省掉人工复核这一步结果误删、误合并的记录比重复记录还多后续返工成本更高。复核的流程我的经验是把候选清单导成CSV同时在GIS软件里叠加卫星影像逐个查看候选对的真实地理位置。如果卫星影像上能看到两栋不同建筑那就判定为合法同名不去重如果影像上只有一个建筑顶点那基本就是重复采集标记为“待合并”。合并时高分记录作为主记录保留低分记录在删除前先挂到主记录的别名表里。每一步处理动作都要落到台账上。我习惯的台账字段是字段说明candidate_id候选组IDpoi_id_a / poi_id_b待处理的两条POIdecision保留/合并/删除/不改reason判断依据例如“影像显示同一栋楼”“两条路物理间隔800米”reviewer复核人check_time复核时间不要小看这份台账。它一方面是为了审计追溯另一方面是给后续增量更新机制提供训练样本。每次人工复核的判断标准都会被沉淀下来时间久了慢慢就能总结出相对稳定的业务规则再反哺到自动排查脚本里。5. 常见问题与避坑实录5.1 容易被误判成重复的三类情况第一种是跨城市合法同名。两个城市各有一条“解放路”它们在地理上没有任何关系只是因为历史命名习惯相同。这类如果被空间近邻聚类捞出来原因多半是坐标阈值设得太大把不同城市的边界都覆盖了。处理方式不是合并而是补行政区划字段。第二种是父子实体关系。一个大型商场“万达广场”里面入驻的商户可能被标成“万达广场3层XX店”。这种记录看起来“同名”其实是“包含关系”不能粗暴去重合并因为他们在地图上的定位、检索语义完全不一样。强行合并会丢掉商户粒度反而破坏用户体验。第三种是链式同义名。现实中一个建筑群东门西南各有一个“城门”它们共享着同一个叫法却是两个完全独立的物理实体。这种只能靠人工结合影像确认脚本再怎么升级也判断不了因为它在逻辑上就是“合法重名”。5.2 搜索引擎引发的“假重复”这个坑做地图产品的人体会最深。用户输入“万达”两个字搜索引擎按分词召回可能把“万达广场”“万达影城”“万达酒店”全部召回。在数据层看这些是完全不同的POI不算重复但在用户界面看他们就是一个“重复结果列表”。解决这个问题的钥匙不在数据清洗而在检索召回策略。通常做法是检索时结合当前定位城市做上下文过滤优先展示同城结果如果用户输入没有行政区划关键词就按“当前城市名称相似度热度”排序而不是纯按全局匹配排序。数据层能做的就是确保每条POI都有准确的行政编码和坐标并且建立好别名与主名的映射关系。5.3 长期维护的几条实用经验地名重复不是清理一次就能根治的状态。我的长期维护经验是在新数据入库的环节就加入同样的去重检查而不是等问题信箱里堆满用户投诉再启动专项治理。第一在应用层加一道“唯一性兜底约束”。对“行政区划编码名称类别”做逻辑唯一键拦截大部分精确重复。当然这个约束不能过于刚性否则会把合法同名实体误伤所以它只能是兜底不能是唯一防线。第二建立操作日志和回滚机制。每次合并、删除POI前都要能一键回滚。我踩过的坑是合并了一批重复POI后下游的导航路径规划出现断链因为旧的poi_id还挂在别的业务表里。后来所有删除操作都改为“逻辑删除”或“停用别名映射”彻底避开引用失效的问题。第三标注团队的录入规范要明确。名称字段只写专名和通名不加门牌号、不加店铺描述。修饰信息放到地址、备注或别名里。这个习惯一旦养成后续去重脚本的压力会小很多。第四每季度做一次批量巡检。跑一遍空间近邻和相似度脚本结合新增数据的评分产出一份增量去重报告。不需要每次都全量重跑只看增量区间就好。我个人这些年最大的体会是地名重复问题没有一次性的终极解法它考验的是数据治理的持续性。只要沉淀好台账、维护好别名、坚持做增量检查再大的POI库也能慢慢被收拾得井井有条。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑