GeoJSONcn
GeoJSONcn / 技术专栏 / 同一块地存成 Polygon 还…

同一块地存成 Polygon 还是 MultiPolygon:全国区县边界几何类型实测

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

打开一份区县文件,随手挑一个街道,看 geometry 那行——有的写着 Polygon,有的写着 MultiPolygon。同样是面,为什么要分两种写法?写解析代码的人要判断两遍,做面积统计的人担心口径不一致,做样式的人又怕规则只命中一半。

三级目录的几何类型分布完全不同

先给一组实测数字。扫描本站 mapjson/ 下三级目录(省 34 个文件、市 376 个、区县 2934 个):

目录要素总数PolygonMultiPolygon单部件要素部件总数
省49804983981590
市36200362032715546
区县422823349287903941354096

规律很清楚:省、市两级清一色 MultiPolygon,34 个省级文件与 376 个市级文件里,没有一个要素写成 Polygon。区县一级正好倒过来,近八成写成 Polygon,MultiPolygon 只有 8790 个。

成因跟数据质量无关,是制图时的处理习惯。沿海省份的轮廓必然带上成片岛屿,一个省拆出几十上百个部件是常态,写 MultiPolygon 最顺手;制作者索性把整级统一成 MultiPolygon,连主体连片的省份也照写。区县一级的来源则杂得多,落到街道、镇这类小单元,多是一整块面,随手存成 Polygon 就交差了。

部件数才是更要紧的指标

Polygon 和 MultiPolygon 的区别只有一个:这个要素是不是被拆成了两组以上的环。GeoJSON 规范里,Polygon 的 coordinates 是"环的数组",MultiPolygon 是"环数组的数组"。一个只有外环、没有内环的要素,用 Polygon 存是 [[[x,y],...]],用 MultiPolygon 存是 [[[[x,y],...]]]——多一层方括号,表达的是同一块地。

所以真正影响处理的不是类型名,是部件数。同样扫一遍,单要素部件数最多的一批:

要素所属部件数
岗尼乡那曲市-安多县104
阿勒腾乡酒泉市-阿克塞哈萨克族自治县92
花土沟镇海西蒙古族藏族自治州-茫崖市76
祁曼塔克乡巴音郭楞蒙古自治州-若羌县72
罗布泊镇巴音郭楞蒙古自治州-若羌县68

这些名字集中在西部地广人稀的地区,一个乡的辖区被大片无人区、山地切成上百个不相连的碎块。换个角度看,93.2% 的区县要素只占一个部件,处理它们跟简单多边形毫无二致。

内环(洞)的情况稀薄得多:区县全库 42282 个要素中,带内环的只有 687 个,占 1.6%;省、市两级分别只有 47 个和 135 个,三级合计 869 个要素、12005 个内环。内环面积通常也极小,占外环面积的中位数只有万分之零点三。但有 157 个要素的洞超过外环的 1%,极端的如鄂温克族自治旗得力其尔鄂温克民族乡,洞吃到外环的 38.54%。洞少见,却足以让个别单元的面积口径出现百分之几十的偏差。

一段能同时吃两种类型的读取代码

下面这段代码把"类型名"和"部件数、环数"一次读出来,不依赖具体是 Polygon 还是 MultiPolygon:

def iter_parts(geom):
    """统一产出 (外环, [内环...]),Polygon / MultiPolygon 通吃。"""
    t = geom.get("type")
    if t == "Polygon":
        polys = [geom.get("coordinates") or []]
    elif t == "MultiPolygon":
        polys = geom.get("coordinates") or []
    else:
        return
    for poly in polys:
        if not poly:
            continue
        yield poly[0], poly[1:]          # 首环是外环,其余按顺序都是内环

def ring_area(ring):
    """平面鞋带公式,返回绝对值(适用于按投影坐标存放的数据)。"""
    s = 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]
        s += x1 * y2 - x2 * y1
    return abs(s) / 2.0

def area_of(geom):
    """外环面积减内环面积,得到真正落地的范围。"""
    total = 0.0
    for outer, holes in iter_parts(geom):
        total += ring_area(outer)
        for h in holes:
            total -= ring_area(h)
    return total

iter_parts 是关键:调用方只需要"外环 + 内环列表",不需要知道原始类型。这样写还有个好处——部件的先后顺序在文件里通常按面积从大到小排(全库 54096 个部件中,只有 1 个例外),所以取 next(iter_parts(g)) 就能拿到主块,不必先算全部面积再排序。

四个容易踩的隐含假设

第一,环的方向不统一,别用它判内外。 有些规范约定外环逆时针、内环顺时针,靠方向就能区分。但这份数据里外环逆时针 36402 个、顺时针 17694 个,两种都有;内环同样混着两个方向。可靠的做法是认"数组里第一个环是外环"这个位置约定,不要拿有符号面积的正负去判。

第二,别假设同一文件内类型统一。 2934 个区县文件里有 1346 个同时混着 Polygon 和 MultiPolygon,占 45.9%。解析器如果写成"先判文件级类型再分支",接近一半文件会读错。

第三,别拿类型名做数据来源的指纹。 同一个名称在不同县下的几何类型可能完全相反:双河镇、青山乡、红旗镇、新城街道 这类高频名,在区县目录里凑出 2016 个"同名但类型不同"的组合。这既不是数据问题,也不能当匹配依据。

第四,内环不能当噪声删掉。 前面那 687 个带洞要素,多数洞面积小到千分之一以下,但碰上鄂温克族自治旗那种占三分之一的,删掉洞等于凭空多算一大块地。稳妥做法是按比例设阈值,只在洞小于外环面积某个千分比时才考虑忽略,并且把命中的要素打印出来人工过一遍。

落到流程里的位置

动手做统计之前,先花几十行脚本跑一份几何画像:要素数、类型分布、单部件占比、最大部件数、带洞要素数、洞面积占比中位数。六个数字出来,下游要不要做特殊处理就有了依据。全国省市区县区划边界数据下载 拿到的若是区县目录,按上表重点看 区县/ 那三列;对 全国村级geojson数据下载 得到的村面数据,单部件占比会再低一些——村界更碎,但单个村被切成一两百块的情形也会同步出现。省市两级则不必费心,无论哪一版数据,一律按 MultiPolygon 解析即可,省掉一次类型分支。

这批几何如果只用于展示、不涉及面积与拓扑运算:这份地图 PPT 模板选购页 里的行政单元在制作时已经把几何写法统一过,拆散的小块也做了归并,预览免费、单份 ¥4.99。要跑统计或判断空间关系的,还是照上面的画像流程自己走一遍更稳。

回到开头:Polygon 还是 MultiPolygon,对下游并不重要,重要的是别假设它统一。用一个迭代器把两种形状归到同一出口,其余判断交给部件数与环数。任何一份从 GeoJSON下载 得到的行政区划数据,都能套用同一套读取逻辑,不必因为来源不同就改代码。