多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

全球经纬度数据包拆解:省市区联动、地图打点与坐标坑

全球经纬度数据包拆解:省市区联动、地图打点与坐标坑 简介一份以全球行政区域层级结构为核心的地理数据包覆盖除中国以外的各国城市地区以及中国全部省市区县含特别行政区每个行政节点均附带经纬度坐标适合地图应用开发、地区联动组件设计、GIS数据处理等场景。压缩包共4个文件总大小仅385KB含2个JSON与2个SQL文件JSON按全球中国以外和中国拆分便于前端读取或离线缓存SQL脚本含建表结构和数据插入语句导入数据库后可直接查询上下级关系及坐标。目前已有376人浏览学习。拿到后无需再手动收集整理多源经纬度信息既能搭建省市区三级联动下拉框也能为地图标注、区域聚合提供稳定的基础坐标数据按需选用相应文件即可。1. 省市区联动、地图打点都要用到的全球经纬度数据包拆开之前先看清这些做省市区联动下拉框、地图打点和地区检索的开发者大概率都经历过同一个窘境为了给地址选择器配上经纬度产品经理丢下一句“全世界都要”剩下的就是自己找数据。免费接口限频严重跨域限制也多下载的世界城市列表又常常国家、省市层级对不齐。这份压缩包解决的就是这个环节——全球各国省市区层级结构带着经纬度一起打包JSON 和 SQL 各一份中国部分单独拆开特别行政区也覆盖在内。前端拿 JSON 直接做级联下拉框和地图打点后端把 SQL 灌进库做地址字典省下几天手工整理清洗的时间。适合电商收货地址、物流区域分拣、地图展示和行政区划筛选这类系统。2. 拆包看货JSON 与 SQL 的分工、字段结构、表关系和坐标系边界拿到压缩包先别急着写代码把四个文件各自的边界搞清楚能省掉后面大半的返工。我一般会先把文件解压到一个独立目录记下文件名然后逐个打开看前几条记录——不是看内容而是看字段名和层级粒度。这个步骤十分钟但能避免后面对错文件。2.1 四个文件的边界全球 JSON、中国 JSON、两份 SQL 各管什么这份压缩包表面上是两个主题实际是四个文件各自面向的使用场景完全不同。我建议按下面这张表来划分它们文件覆盖范围数据粒度主要用途全球各国城市地区层级结构经纬度中国以外.json除中国以外所有国家国家 / 州省 / 城市部分国家到区县前端国家选择器、世界地图打点中国所有省市区层级结构经纬度包括特别行政区.json全国各省市区县省 / 市 / 区县三级齐全国内地址三级联动、收货区域选择全球各国城市地区层级结构经纬度.sql同全球 JSON同全球 JSON后端字典表、数据分析、外部系统对接中国所有省市区层级经纬度表结构与数据SQL包括特别行政区.sql全国各省市区县省 / 市 / 区县三级齐全后端独立地址服务、报表分组最容易踩的第一个坑是把“中国以外”当成了“全世界”。实际上中国数据单独拆出来了如果你直接把全球 JSON 当成全集用中国部分就会缺失最后还得回头补一份中国 JSON 做合并。我一般会在程序里先区分两套数据源一套处理海外地址一套处理国内地址中间用国家编码或地区类型字段做隔离而不是强行拼成一个数组。层级粒度也要提前对齐。全球文件里有些国家只给到“州邦”这一级没有细分到市镇中国文件则到区县数量级完全不同。后续做搜索定位的时候海外记录可以按“国家 州 城市”拼接国内按“省 市 区”拼接不能在代码里笼统地写死四级结构。2.2 JSON 字段与 SQL 建表的对照用平铺结构代替嵌套树的理由这两个 JSON 文件我拿到手看前几条记录之后确认了一个关键点它们是平铺列表不是嵌套树。这个结构其实比嵌套树更实用。嵌套树看起来直观但前端每一次联动都要递归遍历整棵子树后端做筛选和维护也不方便。平铺结构每条记录自带上级关联字段想要什么形状的树自己遍历一次就能拼出来。常规情况下记录字段是这么设计的[ { code: 110000, name: 北京市, parentCode: null, level: 1, lng: 116.4074, lat: 39.9042 }, { code: 110101, name: 东城区, parentCode: 110000, level: 3, lng: 116.4164, lat: 39.9284 } ]注意不同的打包来源字段命名可能有差异有的用id/pid有的用code/parent_code还有的会更直白地用province_code、city_code、district_code三列。这个不重要重要的是看清level字段的取值含义1表示省级或国家级2表示地级市或州3表示区县。拿到数据后先打印前两条记录核对一次字段名写代码时再统一封装一个读取函数避免后续改字段名改到崩溃。SQL 脚本那边一般会对应一张地区表常见的建表结构如下CREATE TABLE region ( id INT NOT NULL AUTO_INCREMENT, code VARCHAR(20) NOT NULL COMMENT 行政区划编码, name VARCHAR(100) NOT NULL COMMENT 地区名称, parent_code VARCHAR(20) DEFAULT NULL COMMENT 上级区划编码, level TINYINT NOT NULL COMMENT 1省/国家 2市/州 3区县, lng DECIMAL(10, 6) DEFAULT NULL COMMENT 经度, lat DECIMAL(10, 6) DEFAULT NULL COMMENT 纬度, PRIMARY KEY (id), UNIQUE KEY uk_code (code), KEY idx_parent_code (parent_code) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 地区层级表;这里有两个值得关注的细节。第一个是经纬度用了DECIMAL(10, 6)精度到小数点后六位大约 0.1 米级别做城市级定位足够比DOUBLE更省空间。第二个是code字段建议建唯一索引因为后续不管是 JSON 还是 SQL在做父子关联时都依赖这个编码而且编码本身就是行政区划标准代码重复的可能性极低。如果你拿到的 SQL 脚本里表名不叫region或者字段是parent_id而不是parent_code都不影响理解只要把后续查询中的字段名对应改掉就行。重点在于确认父子关系是靠code关联而不是靠名称关联——名称会有重名编码不会。2.3 坐标参考系的坑这份数据的经纬度可能是什么坐标系经纬度不是“一个数字”那么简单它背后有一个坐标系问题。行业内常遇到三种坐标系GPS 设备直接输出的 WGS-84 坐标、国内地图服务商普遍使用的 GCJ-02 国测局加密坐标、以及地图平台自定义的二次加密坐标。数据包里如果说明过坐标系就直接用如果没说明那大概率是 WGS-84 或 GCJ-02 二者之一。怎么判断拿五个你已经知道准确位置的城市出来把数据里的经纬度投到地图上比对。如果位置和真实地点几乎重合坐标系就是 WGS-84如果整体向一个方向偏移了几十到几百米那就是经过了国内坐标系加密处理需要做纠偏。这个偏移在单点上看不明显但做“附近地区排序”和“坐标落区判断”时会被放大尤其是在城市边界附近。提示不管最后认定是什么坐标系落库时都不要把经纬度转成整数或截断到小数点后两位存储那会让打点位置偏出去一两公里而且事后没有任何后悔药能找回来。3. 把 JSON 喂给前端联动组件平铺数据转三级索引与就近匹配实战JSON 文件是给前端和脚本用的但这不意味着前端就可以直接拿原始数组渲染。几千条平铺记录挂到界面上让用户翻体验会很糟糕。正确做法是先在后端或构建脚本里把平铺结构转换成三级索引再交给级联组件消费。3.1 一次性遍历构建三级索引省、市、区三张 Map 的写法我习惯用 Python 先做一次预处理把中国 JSON 转成三张索引表。不需要递归一遍循环就够了import json with open(中国所有省市区层级结构经纬度包括特别行政区.json, encodingutf-8) as f: regions json.load(f) province_map {} # code - 省级记录 city_map {} # province_code - { city_code: city_record } district_map {} # city_code - { district_code: district_record } for item in regions: if item.get(level) 1: province_map[item[code]] item elif item.get(level) 2: city_map.setdefault(item.get(parentCode), {})[item[code]] item elif item.get(level) 3: district_map.setdefault(item.get(parentCode), {})[item[code]] item这里的关键是setdefault的用法当parentCode对应的字典还不存在时先创建一个空字典再把当前城市塞进去。遍历结束后city_map的键就是省级编码值就是该省下所有城市的集合district_map同理。整体时间复杂度是 O(n)五千条记录跑完也就是几十毫秒的事。这段预处理代码不一定要写在后端服务里也可以放到前端构建流程中把生成的三张索引序列化成静态文件运行时直接加载。我一般会输出成三个单独的 JSON 文件这样省市区联动组件初始化时不用再遍历一遍全量数据。3.2 联动选择器与完整链路还原从区县一路回溯到国家级联选择器拿到三张索引之后联动逻辑就很机械了选省份时拿到provinceCode从city_map[provinceCode]里取城市列表选城市时拿到cityCode从district_map[cityCode]里取区县列表。这一步本身没难度真正容易翻车的是“根据一个区县 ID 反查省市区完整链路”。很多表单只保存了最末级的区县编码详情页要展示完整地址时就得从下往上回溯def build_chain(code, code_to_region): chain [] current code_to_region.get(code) while current: chain.append(current[name]) current code_to_region.get(current.get(parentCode)) return / .join(reversed(chain)) # 使用时先把所有记录转成 code - region 的字典 code_to_region {item[code]: item for item in regions} full_address build_chain(110101, code_to_region) print(full_address) # 输出北京市 / 东城区这段代码的边界条件在while current处当parentCode为空或找不到上级记录时循环自然终止。要注意的是中国数据里省级记录的parentCode通常为空但全球 JSON 里国家级记录的parentCode也有可能为空两种情况都能正确退出。有一个细节值得注意如果数据里混入了“省直辖县级行政区划”这类特殊节点比如某些地区下面直接挂着县级市而没有中间的地级市build_chain的返回结果会缺一级。处理方式是打印出所有level3但parentCode对应记录level ! 2的异常项单独做映射。这种数据瑕疵几乎每个地区数据包里都存在不是这份文件独有的问题。3.3 附近地区匹配用经纬度做最近城市排序有些业务场景不是“用户选地址”而是“根据用户当前定位反推最近的城市”。比如物流派单要根据 GCJ-02 坐标找最近的配送站点或旅游类应用要显示“离你最近的三个城市”。这时经纬度就派上用场了import math def haversine_km(lat1, lng1, lat2, lng2): radius 6371.0 dlat math.radians(lat2 - lat1) dlng math.radians(lng2 - lng1) a math.sin(dlat / 2) ** 2 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlng / 2) ** 2 return radius * 2 * math.asin(math.sqrt(a)) def nearest_regions(target_lat, target_lng, regions, box0.5, top_n3): candidates [ r for r in regions if abs(r[lat] - target_lat) box and abs(r[lng] - target_lng) box ] candidates.sort(keylambda r: haversine_km(target_lat, target_lng, r[lat], r[lng])) return candidates[:top_n]box参数是经纬度粗筛的矩形半宽。0.5 度大约对应 55 公里对城市级匹配来说是合理的初始范围如果业务只关心同城范围可以缩到 0.1如果数据稀疏、候选太少可以放宽到 1.0。粗筛的意义在于避免对全量数据逐条计算 haversine虽然几千条也不算慢但如果前端的每个请求都全量算一遍后端接口响应时间会随数据量线性恶化。haversine_km用的是球面距离公式对城市级场景误差可以接受。要注意经纬度参数传入顺序lat 是纬度lng 是经度多人协作时很容易传反。我会在函数定义处加一行注释强调否则到运行时地图上定位点飞到非洲大陆排查起来相当耗时。4. 把 SQL 变成后端地址字典导入流程、级联查询与经纬度距离排序后端系统通常不会去解析 JSON 作为主数据源而是把 SQL 脚本导入数据库用一张region表支撑所有地址查询。这章说清楚导入过程、常见查询写法和距离排序的优化方式。4.1 导入 MySQL字符集、文件名与导入后的数量校验导入前先做两件事把 SQL 文件名改成纯英文避免终端在解析含中文路径时出问题然后确认脚本的字符集声明。我一般会在 MySQL 交互终端里执行mysql -u root -p --default-character-setutf8mb4进入 MySQL 之后再执行导入CREATE DATABASE IF NOT EXISTS geo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE geo; SET NAMES utf8mb4; SOURCE /tmp/regions_china.sql;SET NAMES utf8mb4这一步相当关键它告诉客户端和服务器之间的连接使用全量 Unicode 编码。如果漏掉这一步脚本里的中文地区名很可能在导入过程中被转成错误字节落库后变成??或乱码。utf8mb4_unicode_ci排序规则对中文地区名足够用不需要额外配置。导入完成后先做数量校验SELECT level, COUNT(*) AS cnt FROM region GROUP BY level;正常情况下中国文件里level1的记录应该是 34 条左右含省、自治区、直辖市、特别行政区level3的区县记录在两千八百条上下。如果数量级差得离谱比如省级只有 20 多条说明脚本被截断或导错了文件需要重新导入而不是急着写业务代码。全球 SQL 那边的数量没有固定标准因为不同国家行政区划细度不一致但导入后也要跑一遍同样的GROUP BY level确认没有空数据。4.2 后端查询一次性全量加载还是 JOIN 联查地区表的数据量即使在全世界范围内也只有几万到几十万条完全属于“小表”。所以我不建议用递归 CTE 去查这棵树也不建议在 SQL 里做递归遍历。最省事的方案是后端服务启动时把整张表加载进内存用dict按code索引业务查询全部走内存。如果数据量增长到百万级再考虑引入 Redis 缓存或切换存储方案。但有些场景确实绕不开 SQL 查询比如报表系统要根据某个区县的名称反查它属于哪个省。这时一条 JOIN 就能解决SELECT p.name AS province_name, c.name AS city_name, d.name AS district_name, d.lng, d.lat FROM region d LEFT JOIN region c ON d.parent_code c.code LEFT JOIN region p ON c.parent_code p.code WHERE d.name 目标区县名;这里有个很隐蔽的坑区县名称在全国范围内大量重名。城关区、鼓楼区、新城区这类名字可能同时出现在好几个省直接用WHERE d.name 目标区县名会返回多条记录。解决方式是查询条件里带上父级城市名或者在业务端展示搜索候选让用户确认不能拿名称当唯一键。另一个注意事项香港、澳门、台湾的记录层级和大陆不同港澳只有“省级→区级”两级台湾有“省级→县市→区”三级的特殊结构。JOIN 查询在处理这些记录时中间的城市级 JOIN 会返回空需要靠LEFT JOIN而不是INNER JOIN保住结果集不丢失。写报表时对港澳台记录单独做兼容不要硬套大陆“省市区”三段逻辑。4.3 经纬度距离排序箱式粗筛 Haversine 精排“按距离最近的城市排序”在 SQL 里也有对应写法。直接对全表做距离计算然后排序在小表上能跑但不够优雅而且随数据量增长性能会快速下降。我惯用的方式是两层筛选先用BETWEEN把经纬度圈在一个矩形范围内再在结果集上计算精确距离并排序SELECT name, lng, lat, 6371 * 2 * ASIN( SQRT( POWER(SIN((RADIANS(39.9042) - RADIANS(lat)) / 2), 2) COS(RADIANS(39.9042)) * COS(RADIANS(lat)) * POWER(SIN((RADIANS(116.4074) - RADIANS(lng)) / 2), 2) ) ) AS distance_km FROM region WHERE lat BETWEEN 38.9 AND 40.9 AND lng BETWEEN 115.4 AND 117.4 ORDER BY distance_km ASC LIMIT 5;BETWEEN圈定的是一个经纬度矩形四个数字分别是目标点纬度减/加一、经度减/加一。纬度一度约 111 公里这里圈的是大概 222 公里见方的范围足够覆盖城市群级别的候选集。如果只想搜五十公里内的地区就把BETWEEN范围缩到加减 0.5。这个查询的性能瓶颈在ORDER BY distance_km上它要对圈定范围内的每一行计算三角函数无法走索引。在几千行的小表上毫秒级完成不用担心但如果全球数据导入后有几十万行且每次请求都从全表圈范围建议在lng和lat上建联合索引。注意只有当矩形范围过滤掉大部分数据时索引才有效如果目标点在大洋中央一圈就把全世界都圈进去了那就回到全表计算的老路。5. 地区数据避坑实录乱码、直辖市断层、港澳台字段不一致等五个折腾点这类数据包的坑不在代码逻辑而在数据本身的特殊边界。这章写五个我实际遇到过的问题每个都按“现象→原因→解决”来拆看完能少走几趟回头路。5.1 导入 SQL 后全是乱码地区名称变成“??”或“锟斤拷”现象SQL 导入执行成功但SELECT * FROM region LIMIT 10显示的名称全是问号或乱码字符串稍微好一点的情况是表和库的字符集设置正常但数据内容已经是不可逆的错误字节。原因绝大多数情况是导入时客户端连接字符集没指定为utf8mb4。很多 SQL 文件头部的注释虽然写着SET NAMES utf8mb4但如果你用图形化工具导入或者SOURCE前没有手动执行SET NAMES连接字符集会沿用默认的latin1或系统变量设置中文被按单字节解析落库就成了乱码。另外如果你用某些编辑器打开过 SQL 文件并“另存为”过文件被转成了带 BOM 的 UTF-8也会导致首条语句报错或注释错位。解决导入前先SET NAMES utf8mb4;再SOURCE这是最可靠的方式。导入后立刻抽查三条中文记录不要等业务上线后才发现。如果已经导乱了先DROP TABLE清掉表重新走一遍正确流程不要试图用UPDATE去修复乱码字节已经损坏改不回来的。5.2 直辖市和“直筒子市”只有两级三级联动出现断层现象级联组件里北京、上海、天津、重庆下面直接挂着区没有“市辖区”这一中间层东莞、中山、儋州这类地级市下面也没有区县节点。用户选择省之后城市下拉框有值但选了城市之后区县下拉框是空的。原因行政区划本身就是这样设计的直辖市和“直筒子市”没有地级中间层数据忠实反映了现实。如果前端组件把“省→市→区”当成固定三级强关联遇到只有两级的节点就会断掉。解决在联动组件的选择逻辑里做一个兜底判断当选中的城市没有对应子节点时自动把该城市本身复制为区县选项或者直接把城市名称填到区县字段里。后端做地址解析时也要兼容“只有省级区级”的链条build_chain里少一层中间节点不能报错要能正常输出。5.3 港澳台与特别行政区层级和大陆对不上查询结果为空现象按“省→市→区”三级逐层查询香港省能查到但市一级是空的按大陆的level3标准去查台湾某些区也查不到记录。特别行政区的记录有的挂在level1下面直接跟着level3跳过了一层。原因港澳台行政区划没有“地级市”这一级香港特别行政区下面直接是十八个区澳门更小台湾是“省→县市→乡镇市区”三级结构但节点名称和大陆习惯不同。这份数据混在同一张表里用统一的level字段表达不同宪制结构必然会出现层级跳空。解决把港澳台数据单独做成一套映射表或平行数据结构解析时先判断code前缀。香港是810000开头澳门820000台湾710000或710000相关编码。匹配到这些前缀就走“省级→区级”的两级逻辑不走大陆三级逻辑。另外注意繁体简体转换搜索“香港特别行政区”和“香港特別行政區”可能查出完全不同的结果。5.4 拿“中心点”当行政边界用户定位落到了隔壁区现象地图上打点后反查用户所在行政区返回的区和用户实际位置不一致。比如明明在 A 区边缘数据判断结果却是相邻的 B 区。原因这份数据里的经纬度是“代表点”可能是政府驻地或几何中心不是行政区域边界。用中心点判断一个坐标“属于哪里”本质上是用点代替面在区域边缘必然出错。这个误差不是数据质量差导致的是所有不带边界 Polygon 的经纬度数据集的通病。解决判断坐标落区的需求必须引入行政区划边界数据GeoJSON 格式做真正的点面包含判断这份数据的经纬度只适合做展示、定位、距离排序。如果临时需要近似判断可以退一步用中心点和半径做一个粗略归属半径以内的坐标都算“周边地区”但产品文案要写成“附近”而不是“所在”。5.5 JSON 在 Windows 打开乱码代码读取也会偶发报错现象Windows 上双击 JSON 文件用记事本打开中文显示乱码代码里open()读取时报UnicodeDecodeError或读到开头有\ufeff之类的奇怪字符。原因JSON 文件是 UTF-8 无 BOM 编码Windows 记事本默认按本地 ANSI 编码打开就会显示乱码。某些编辑器保存时会自作主张加上 BOM 头Python 的json.load()读取时会把\ufeff当成非法字符导致解析失败。解决用编辑器打开时手动切到 UTF-8 编码代码里读取统一加encodingutf-8如果担心 BOM 头用encodingutf-8-sig读取更保险with open(global_regions.json, r, encodingutf-8-sig) as f: regions json.load(f)utf-8-sig会自动去掉开头的 BOM 标记对无 BOM 的文件也兼容。从血泪经验来说我后来养成的习惯是任何地区数据文件到手后先file命令看一下编码和换行符确认是 UTF-8 后再喂给程序或数据库。6. 进阶坐标“落区”判断的边界、坐标系纠偏与我的固定验收流程数据本身能跑起来只是第一步能不能在真实业务里站住脚取决于你是否清楚这份数据的能力边界。最后一章写三个我常用的进阶验证手段以及我现在处理任何地区数据包都强制走一遍的检查习惯。6.1 先做一个坐标合法性校验器经纬度范围与大陆矩形粗筛任何业务在持久化坐标数据前都应该先过一层合法性和疑似异常检查。我通常在数据接入层写一个很小的校验函数def validate_coords(lng, lat): if not (-180 lng 180 and -90 lat 90): return invalid # 中国大陆大致范围东经73°~135°北纬18°~54° if 73 lng 135 and 18 lat 54: return china_mainland return overseas这个函数不解决精度问题但能拦下最明显的脏数据字段错位、坐标写反、正负号丢失。跑批任务里如果一批数据全是非洲大陆的坐标但业务明确是国内打点那基本就是经纬度字段传反了——这在高频踩坑榜单上排前三。6.2 坐标系不一致时的纠偏顺序判断、转换、复验如果抽样比对确认数据和目标地图坐标系不一致不要直接改数据文件里的坐标而是在业务层做一层转换。转换前先确定源坐标系和目标坐标系转换后再抽五个点反向验证。常见的做法是从 WGS-84 转到目标平台坐标或者从目标平台坐标还原到 WGS-84。纠偏算法各家实现大同小异但顺序必须固定统一先转 WGS-84再转目标坐标系中间不要跨坐标系跳跃。提示坐标纠偏的结果精度约在几米到几十米之间城市级应用够用做车道级定位就需要更精确的差分数据了这份包不适合。6.3 固定验收流程四步冒烟测试避免后续返工我处理每个地区数据包的固定顺序是这样的。第一步COUNT(*)校验记录数中国省级约 34 条区县级两千八百条上下数量不对就停下来排查。第二步编码校验抽几条记录的code前缀确认香港是 81、澳门是 82、台湾是 71 开头和国标保持一致。第三步坐标抽样随机取五个省会城市把经纬度和地图比对误差超过一公里就认为坐标系有疑问必须在上线前澄清。第四步级联冒烟启动前端组件从省选到区走一遍尤其关注北京、东莞、香港这三类特殊结构确认没有空下拉框。从那以后我每次接地区数据包都会强制把四步检查走一遍哪怕再赶也至少跑完前两步。别嫌这环节琐碎正是这些不起眼的核对拦住了大部分上线后才发现的结构性翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表