GeoJSONcn
GeoJSONcn / 技术专栏 / 全国省市区县区划边界数据下载后先…

全国省市区县区划边界数据下载后先算一遍共享边:一半的公共边被画了两遍

发布于 2026-09-24 · GeoJSONcn 技术团队 · 阅读约 19 分钟

拿到一批乡镇街道边界,多数人先看要素数、看字段、看面积合计,很少有人去看相邻两个乡镇之间那条公共边,在文件里到底被存了几次。这个看似无关紧要的细节,决定了后面能不能做拓扑合并、能不能按邻接关系建图、能不能把两个乡镇拼成一个片区而不留缝。 把本站 mapjson/区县/ 下 2901 个有效文件全量扫一遍(42282 个要素、4096 万条边),结论是:唯一化之后剩下 2704 万条边,其中 1391.5 万条恰好出现过两次,48.5% 的边只出现过一次。换句话说,全国乡镇边界数据下载 拿到的文件里,公共边大约有一半是两边各画各的,而不是共用同一组顶点。

一条边出现两次是正常的,出现一次才是要留意的

先用 edge_key 把每条边按两个端点的坐标量化成无序键,同一个文件内统计每个键的出现次数。理想情况下,同一条公共边会被相邻两个要素各引用一次,频次恰好是 2。

全量统计出来的频次分布:

边的出现频次边数含义
1 次13,119,960未配对,两侧不共用顶点
2 次13,915,230正常共享,两要素各一次
3 次3,346三要素交汇处的短边
4 次585四要素交汇
5 次及以上107极罕见,需人工确认
合计27,039,228唯一化后的边总数

按长度而不是条数来算,差距更直观:共享边总长 978,012 千米,未配对边总长 1,083,063 千米,未配对边的长度占比 52.6%——和条数口径的 48.5% 基本一致,说明这不是被少数极短边拉偏的结果。

出问题的不是那些频次 1 的边本身。凡是落在县域外圈、海岸线、国界线上的边,本来就只属于一个要素,出现一次是对的。真正需要留意的是应该配对却没配上的那部分:它们出现在县域内部,是两个相邻乡镇之间的公共边,却各自带一套顶点。

import json, collections

def edge_key(a, b):
    """相邻两点量化到 1e-6 度后取无序键;两个方向视为同一条边"""
    p = (round(a[0], 6), round(a[1], 6))
    q = (round(b[0], 6), round(b[1], 6))
    return (p, q) if p <= q else (q, p)

def scan(path):
    d = json.load(open(path, encoding="utf-8"))
    cnt = collections.Counter()
    for f in d["features"]:
        gm = f.get("geometry")
        if not gm:
            continue
        polys = gm["coordinates"] if gm["type"] == "MultiPolygon" else [gm["coordinates"]]
        for poly in polys:
            ring = poly[0]                     # 只看外环,内环另算
            for i in range(len(ring) - 1):
                cnt[edge_key(ring[i], ring[i + 1])] += 1
    shared = sum(1 for v in cnt.values() if v >= 2)
    return len(cnt), shared, shared / len(cnt) if cnt else 0.0

同一个国家数据,共享比能从 97% 掉到 1.6%

把每个文件的共享边占比单独算出来排序,分布不是"普遍偏低"或"普遍正常",而是两极分化。同一份全国数据里,两种极端同时存在。

区县要素数唯一边数共享边数共享占比
贵州·黔西市3123,92623,21297.0%
贵州·金沙县2518,10717,19094.9%
湖北·蕲春县1613,68712,96494.7%
贵州·大方县3715,80014,79493.6%
湖南·醴陵市3813,31512,38493.0%
…
四川·射洪市236,4031011.6%
四川·大竹县318,8471421.6%
四川·金堂县163,137591.9%
四川·威远县143,429401.2%
四川·和布克赛尔蒙古自治县152,04350.24%

黔西市和黔西县这两份文件内容完全一致(31 个要素、23,926 条唯一边、23,212 条共享),是同一轮数据在两个文件名下各存了一份,属于文件名层面的冗余,与拓扑无关。

真正值得看的是四川那一列。大竹县 31 个乡镇,唯一边 8847 条,共享的只有 142 条,占 1.6%;顶点层面同样,8352 个唯一顶点里只有 662 个被两个以上要素共用,占比 7.9%。再对比它逐要素的共享长度占比:四合镇 0.3%、天城镇 0.6%、月华镇 1.0%,最高的团坝镇也只有 7.8%。

这说明大竹县这 31 个乡镇的边界是各自独立矢量化出来的。相邻两个镇之间那条线,在这份文件里真实存在两份几何:一份属于左边那个镇,一份属于右边那个镇,两套顶点在数值上并不完全相同。

独立矢量化带来的三个后果

拼合会出缝或出重叠。 把两个相邻乡镇按 union 合并,如果公共边不是共用顶点,浮点运算下会在接缝处留下长度以米计的细缝,或者留下一条极窄的重叠带。用这条缝去算面积,误差可以忽略;但拿去做进一步的空间运算(缓冲、裁剪、再合并),缝隙会逐级放大,最后出现几何自相交。

邻接关系必须靠几何判定,不能靠顶点相等。 判断"哪两个乡镇相邻"最省事的做法是查是否有共享边。在黔西市这类文件上,这个方法直接可用,一个哈希表就出结果。在大竹县这类文件上,共享边判据几乎全部落空,必须退回 intersects 或按最小距离阈值判定,计算量从 O(n) 涨到 O(n²)。做 全国省市区县geojson数据下载 后续分析的,这一条直接决定代码怎么写。

同一个文件的两种约定不能混用。 更麻烦的是同一批数据里两种约定都存在的县。按要素共享长度占比排序后,一个县内往往前几个要素接近 0%、后几个接近 20%——说明该县一部分乡镇做过拓扑对齐,另一部分没有。这种情况下按统一阈值做邻接判定,会漏掉那些本来相邻但没对齐的要素对。

至于怎么处理,取决于用途:只做展示的,保留原样即可,视觉上看不出差别;要做拓扑运算的,得跑一遍节点对齐(把落入容差范围内的顶点吸附成同一坐标)或直接用拓扑修复工具重建。判据本身很好写——算一遍共享比,低于某个线(经验值 45% 以下)就说明这份文件没做过拓扑对齐,后续运算要走容错路径。这条规则对 GeoJSON下载 回来的任何一批乡镇级数据都成立,与数据出自哪个来源无关。

地域分布上这个比例有规律可循。按地级单位聚合,毕节市 91.5%、黔西南州 82.3%、黄冈市 75.9%、重庆市 73.6% 排在前列;而伊春市 9.0%、双鸭山市 8.0%、克拉玛依市 6.4% 垫底。需要说明的是,这个比值与地形碎度强相关:海岛县和沿海县因为外圈海岸线本身就占掉大量边,共享比天然偏低——烟台长岛县 8 个要素、505 条唯一边,共享边为 0;大连长海县 5 个要素、4353 条唯一边,同样为 0。这类不该被当成"未对齐",判定时要先把面积占比最大的那个要素识别成主岛,只对内部边界计分。

落到流程里的取用方式

如果只是需要某个市或某个区县的地图底图摆进材料,不必在拓扑上花时间。这类需求可以直接取现成的排版件——这里按省、市、区县三级列好了可单独改色的底图,一份 ¥4.99,预览先看不花钱,那里的分块在制作阶段就做过节点对齐,相邻两块拼起来不会有缝。

共享比这个指标的价值在于它几乎不花成本。一次全量扫描只读几何、不建索引,比算一遍面积还快,却能同时给出三个判断:这份 全国省市区县区划边界数据下载 得到的文件能不能直接做拓扑运算、邻接判定该走哪种算法、以及同一个县内部是否存在混合约定。等到逐个县跑完修复再回头定位问题,花的力气要大一个量级。

拓扑约定不统一这件事,在字段和要素数上完全看不出来,只有在算过共享边之后才现形。全国村级geojson数据下载 得到的村界在这一点上更明显——村面的独立矢量化比例通常高于乡镇,聚合到乡镇一级时若没做节点对齐,原先的接缝会原样保留下来。各级行政区划的口径,仍应以国家有关部门发布的为准。