全国乡镇街道边界数据下载后先查同县同名:817 组要素,多出来的那条是谁
一份区县文件打开来,要素数几十个,看起来就是该县的乡镇街道全集。但把 NAME 列单独拎出来数一遍,会发现有些名字在同一个文件里出现了两次甚至三次。本站 site/mapjson/区县/ 的 2934 个文件、42282 个要素里,这类同县同名共 817 组、涉及 1659 个要素,也就是有 842 条是"多出来的"。它们不是简单重复:code 列在同一组内 816 组完全一致,几何却各不相同,面积能差出四个数量级。
先分清重名与重复
series-63 谈过全站级的 code 重复——同一份文件里两条要素代码一样、几何也基本重合,那种是纯粹的冗余。同县同名是另一回事:code 一致说明它们指向同一个行政区划实体,可几何是不同来源、不同精细度的两套轮廓被同时塞进了文件。全站 817 组里只有 1 组的组内 code 不统一,其余 code 完全对齐,所以按码去重起不到作用,按名去重又会误伤——判据必须落到几何上。
以四川省达州市万源市为例,70 个要素、52 个唯一名称,16 组重名:
| 名称 | 条数 | 各条外环面积(km²) | 各条顶点数 |
|---|---|---|---|
| 庙子乡 | 3 | 0.0000 / 124.9904 / 0.0013 | 63 / 638 / 715 |
| 大竹镇 | 3 | 0.0029 / 120.5272 / 0.0007 | 212 / 508 / 383 |
| 曹家乡 | 2 | 0.0005 / 121.3218 | 288 / 362 |
| 八台乡 | 2 | 0.0090 / 57.1767 | 66 / 143 |
| 长石乡 | 2 | 107.2779 / 0.0003 | 119 / 67 |
三条 code 全是 511781226000,grade 全是 4,类型 全是"乡镇"。面积最大的那条是行政意义上的乡域本体;另外两条加起来不到 0.0014 平方千米,顶点数却一点不少——庙子乡的小条有 63 个顶点、13 个部件,是十几块互不相连的碎斑。这类小条从哪来的可能性有两个:一是边界数字化时留下的碎块被单独存成了要素,二是乡镇合并后旧界址未清理干净。无论哪种,把它们当作独立乡镇参与统计都会出错。
组内角色:主面、碎斑与残留片
按外环面积把每组排个序,最大的那条记作主面,其余 842 条落在三个区间里:
- 面积不足主面千分之一的 365 条,多为几平方千米以下、形状零碎的残留;
- 占主面千分之一到两成的 473 条,中位面积 0.264 平方千米、九成不到 2.93 平方千米;
- 真正能与主面等量齐观的只有 4 条,这四例需要单独看。
从几何位置看,838 条非主面条的包围盒与主面重叠过半,说明它们落在主面的范围里,属于"主面内部的碎块"。剩下 4 条要格外小心,其中两例相距并不近:新源县则克台镇的两条 code 分别落在 654025104000 与 654025101000——不同代码,两条重心相距 68.6 千米,面积 601.92 与 228.95 平方千米,是相邻镇的同名,不该被合并;铜山区马坡镇、茅村镇两条 code 相同,但重心分别相距 5.65 与 8.14 千米,包围盒几乎不重叠,面积比 10.5% 与 0.3%,更像是同一个镇在不同年代的两套界线。
清洗逻辑写成这样比较稳:
import json, math
from collections import defaultdict
R = 6371.0088
def ring_area(ring):
if len(ring) < 4:
return 0.0
t = 0.0
for i in range(len(ring) - 1):
x1, y1 = ring[i][0], ring[i][1]
x2, y2 = ring[i + 1][0], ring[i + 1][1]
t += math.radians(x2 - x1) * (2 + math.sin(math.radians(y1)) + math.sin(math.radians(y2)))
return abs(t * R * R / 2.0)
def outer_area(geom):
if not geom:
return 0.0
c = geom["coordinates"]
polys = [c] if geom["type"] == "Polygon" else c
return sum(ring_area(p[0]) for p in polys if p)
def dominant(features, min_vert=100):
"""组内选主面:先看代码是否多种,再看面积与顶点量。
返回 (主要素下标, 需人工复核的候选下标列表)"""
codes = {str((f["properties"] or {}).get("code") or "") for f in features}
areas = [outer_area(f.get("geometry")) for f in features]
verts = [sum(len(r) for p in _parts(f["geometry"]) for r in p) for f in features]
big = max(range(len(features)), key=lambda i: areas[i])
if big == 0 and len(codes) > 1:
# 代码不同的同名,是真实的两个单元,不合并
return None, list(range(len(features)))
soft = [i for i in range(len(features))
if i != big and verts[i] > min_vert and areas[i] > areas[big] * 0.2]
return big, soft
def _parts(geom):
if not geom:
return []
c = geom["coordinates"]
return [c] if geom["type"] == "Polygon" else list(c)
def audit(county_path):
gj = json.load(open(county_path, encoding="utf-8"))
buckets = defaultdict(list)
for ft in gj["features"]:
buckets[str((ft["properties"] or {}).get("NAME") or "")].append(ft)
out = {}
for name, fts in buckets.items():
if len(fts) > 1:
out[name] = dominant(fts)
return out
min_vert 取 100 是经验值:万源市那 16 组里,主面顶点数都在 100 ~ 638 之间,碎斑则两极分化——要么只有几十个点,要么顶点很多但部件数也很多(庙子乡的碎斑 715 个顶点摊在 41 个部件上,平均每块 17 个点)。
命名口径在三级目录里并不统一
顺带一个容易漏掉的点:省/、市/、区县/ 三级的属性列结构并不一致。省/ 下 340 个要素用 省、省代码、类型,另 124 个用 FID、NAME、PAC;市/ 下 3245 个走 NAME,375 个走 市、市代码;到了 区县/,41659 个要素是 NAME 列,另有 600 个用 Name——大小写不同。写通用读取函数时 p["name"] or p["NAME"] or p["Name"] 这种兜底必须带全,否则那 600 个要素整条被跳过。
另外,全站 41718 个乡级要素里,code 列完全为空的 36 条,分布非常集中:国营新兴县岩头林场、安徽亳州工业园区、六安经济开发区、黄山风景区、五峰山林场、雅达虹工业集中区……清一色是林场、开发区、风景区一类非正式乡级单元,grade 与 layer 两列也是空值。也就是说这批要素天生没有 12 位乡级代码可挂,不是漏填。处理时按"名称可用、几何有效、代码待补"单独入队,不要混进正式乡级做面积求和。
同县同名的处理结果同样会影响体积。154 个受影响文件里,按名称取面积最大者之后,顶点数从 630303 降到 457966,冗余 172337 个顶点、占 27.34%。延边汪清县 18805 个顶点里有 8335 个属于多余条(44.3%),若羌县 42.1%,安多县 39.2%。这些顶点对渲染没有任何贡献,一次性清掉比在浏览器端做逐点判定划算得多。
落到流程里的位置
一套可复用的次序是:先按 NAME 分组,再对每组比对 code 集合——代码不唯一的分到"疑似同名不同地",代码唯一的进入候选合并;候选组里按外环面积定主面,面积比低于千分之一的直接剔除,其余留出人工确认名单。最后跑一次面积闭环,用县级要素合计与公开县面积对比,差值超过 1% 就回查这一步是不是删错了。
如果手上这批数据只是要拿来做展示,这套审查完全可以不做:这份地图 PPT 模板选购页 里的底图在制作阶段已经按行政单元排好,同县同名的碎块要么并进主面、要么直接去掉,预览免费、单份 ¥4.99,省掉的是逐县核对的时间。要做空间分析或面积统计的,还是得自己跑一遍。
817 组这个数字跑在本站 mapjson/区县/ 的当前快照上,换一版数据源会变。可带走的判据是"代码集合 + 面积比 + 顶点量"这三项,它们不依赖文件名、不依赖命名列大小写,从 全国省市区县区划边界数据下载 得到任何一份乡镇级文件都能直接套用;对 全国村级 geojson 数据下载 得到的村面数据更要跑一遍,村级同名比乡镇级密集得多,一个县里出现三五个同名村组是常事。至于单纯比对省市两级轮廓、不做面积核算的场景,GeoJSON下载 之后直接跳过这一步也无妨,那两级几乎没有同县同名的困扰。