OpenStreetMap logo OpenStreetMap

作为一名贡献者,我一直致力于提升本地 OSM 数据的细节和可用性。但是最近我发现一个令人困扰的问题:在我绘制过的区域,很多地点的中文名称在某些软件/服务中 (比如 OsmAPP、JawgMaps 和 MapTiler 的瓦片等)无法正确显示(回退为拼音),或是优先显示了英文名 (name:en),导致看起来怪怪的,明明有 name 但就是不用。

主要原因这些软件的神必渲染规则通常会优先寻找符合用户语言的 name:[lang] 标签。虽然我们加了 name 标签,但如果缺少了明确的 name:zhname:zh-Hans 标签,渲染器可能就会“不知所措”,转而去寻找 name:en 或干脆直接显示拼音。

手动为成千上万个要素添加这些标签显然是不现实的,又不能靠复制粘贴,一圈下来人可能都麻了。我决定靠自动化,也就是写一个脚本来解决这个问题。

技术选型与脚本逻辑

对于这种讲究高性能的操作,C++ 肯定是首选,我还用了两个强大的开源库:

  1. pugixml: 一个以轻量和高性能著称的 C++ XML 解析库,用来极速读写庞大的 .osm 文件。
  2. OpenCC: 社区公认的中文简繁转换标准库,用于生成 name:zh-Hant 标签。

我编写的脚本核心逻辑如下:

  1. 读取与解析: 使用 pugixml 加载从 Overpass API 查询的的本地 .osm 数据文件;
  2. 遍历要素: 循环遍历文件中的每一个 node wayrelation
  3. 定位目标: 检查元素是否含有 k="name"tag
  4. 生成标签: 如果找到 name 标签,则执行以下操作:
    • 复制 name 标签的值,创建新的 <tag k="name:zh" v="..."/>
    • 再次复制 name 标签的值,创建新的 <tag k="name:zh-Hans" v="..."/>
    • 调用 OpenCC 库(使用 s2twp.json),将 name 的值从简体中文转换为台湾地区通行的繁体中文,并创建 <tag k="name:zh-Hant" v="..."/>
  5. 生成变更文件: 将所有被修改的要素(保留其原始 version 号)写入一个全新的 .osc (osmChange) 文件中,以便上传。

这里我贴一个 AI 生成的代码 (懒得写),各位可以参考一下:

#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <memory>
#include <vector>
#include "pugixml.hpp"
#include <opencc/SimpleConverter.hpp>
#include <opencc/Exception.hpp>

int main(int argc, char* argv[]) {
    
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <input.osm> <output.osc>" << std::endl;
        return 1;
    }

    const char* input_file = argv[1];
    const char* output_file = argv[2];

    
    opencc::SimpleConverter converter("s2twp.json");
    
    
    pugi::xml_document doc_in;
    pugi::xml_parse_result result = doc_in.load_file(input_file);

    if (!result) {
        std::cerr << "Error parsing input file: " << result.description() << " at offset " << result.offset << std::endl;
        return 1;
    }

    std::cout << "Successfully parsed input file: " << input_file << std::endl;

    pugi::xml_node osm_node = doc_in.child("osm");
    if (!osm_node) {
        std::cerr << "Error: <osm> root tag not found." << std::endl;
        return 1;
    }

    
    std::vector<pugi::xml_node> modified_elements;

    
    for (pugi::xml_node element : osm_node.children()) {
        if (strcmp(element.name(), "node") != 0 &&
            strcmp(element.name(), "way") != 0 &&
            strcmp(element.name(), "relation") != 0) {
            continue; 
        }

        bool has_name_tag = false;
        std::string name_value;

        
        for (const auto& tag : element.children("tag")) {
            if (strcmp(tag.attribute("k").as_string(), "name") == 0) {
                has_name_tag = true;
                name_value = tag.attribute("v").as_string();
                break; 
            }
        }
        
        
        if (has_name_tag) {
            
            pugi::xml_node tag_zh = element.append_child("tag");
            tag_zh.append_attribute("k") = "name:zh";
            tag_zh.append_attribute("v") = name_value.c_str();

            pugi::xml_node tag_zh_hans = element.append_child("tag");
            tag_zh_hans.append_attribute("k") = "name:zh-Hans";
            tag_zh_hans.append_attribute("v") = name_value.c_str();
            
            
            try {
                std::string name_hant = converter.Convert(name_value);
                pugi::xml_node tag_zh_hant = element.append_child("tag");
                tag_zh_hant.append_attribute("k") = "name:zh-Hant";
                tag_zh_hant.append_attribute("v") = name_hant.c_str();
            } catch (const opencc::Exception& e) {
                std::cerr << "Warning: OpenCC conversion failed for value '" << name_value << "'. Error: " << e.what() << std::endl;
                
            }

            modified_elements.push_back(element);
        }
    }
    
    std::cout << "Found and processed " << modified_elements.size() << " elements with 'name' tag." << std::endl;

    
    if (!modified_elements.empty()) {
        pugi::xml_document doc_out;
        auto declarationNode = doc_out.append_child(pugi::node_declaration);
        declarationNode.append_attribute("version") = "1.0";
        declarationNode.append_attribute("encoding") = "UTF-8";

        pugi::xml_node osm_change_node = doc_out.append_child("osmChange");
        osm_change_node.append_attribute("version") = "0.6";
        osm_change_node.append_attribute("generator") = "osm_name_tool_cpp";

        pugi::xml_node modify_node = osm_change_node.append_child("modify");

        for (const auto& el : modified_elements) {
            modify_node.append_copy(el);
        }

        if (doc_out.save_file(output_file, "  ")) {
            std::cout << "Successfully generated osmChange file: " << output_file << std::endl;
        } else {
            std::cerr << "Error writing to output file: " << output_file << std::endl;
            return 1;
        }
    } else {
        std::cout << "No elements with 'name' tag found. Output file was not created." << std::endl;
    }

    return 0;
}

结果

脚本不到 1s 的时间内高效地完成了任务。在大竹县区域内 (我从 Overpass 查询出来大小是 30 多M),它为 1790 个要素(包括 599 个节点,1110 条道路和 81 个关系)添加了缺失的中文语言标签。

我已将生成的 .osc 文件通过 Vespucci 上传,相关变更位于这个变更集中。

Screenshot_2025_0914_135956.png

比较

这里有一组用脚本编辑之前与之后的对比,可供参考 (图中所展示的是 OsmAPP,“一个通用的 OpenStreetMap 应用程序”):

编辑之前 编辑之后
Screenshot_2025_1001_172636.png Screenshot_2025_1001_172604.png

用脚本带来的潜在问题与风险

  • 非中文名称: 脚本目前比较初级,它没有检查 name 标签本身是否为英文或其他非中文字符(例如 name="KFC")。虽然在我熟悉的区域内这种情况几乎没有,但不能完全排除错误处理的可能性。
  • 简繁转换错误: OpenCC 虽然强大,但不是万能的。某些特定地名的转换可能会出错。

欢迎审查与反馈

我进行这次编辑的初衷是为了让 OSM 数据在更广泛的应用中发挥价值,提升最终用户的地图体验。

我非常欢迎,并恳请社区的各位 mapper 帮助审查这个变更集,如果你发现了任何不当的修改、错误的数据或有更好的处理建议,请不要犹豫,直接在变更集上留 comment 或通过 OSM 站内信联系我。

对于任何指出的问题,我都会积极跟进,进行修正甚至是靠 osm-revert 回滚。谢谢。

Email icon Bluesky Icon Facebook Icon LinkedIn Icon Mastodon Icon Telegram Icon X Icon

Discussion

Comment from 真中あお on 11 December 2025 at 17:28

我很赞同该批量修正一下缺失name:zh的问题! 不过,从流程上来讲,使用程序批量编辑时应当先讨论取得共识,再执行。 鉴于用户日记很少有人互动,我觉得在OSM中国社群的电报群(id编辑器上每次提交编辑后会显示加群链接)或QQ群(群号290278518)可能会比较容易找到人完成讨论~

Log in to leave a comment