Nominatim 通过 ICU 音译(Transliteration) 机制处理中文,核心思路是:将中文转为拼音(Latin 形式),然后用拼音 token 在数据库中进行匹配。
一、导入阶段(Indexing)—— 中文名称如何入库
以 OSM 标签 name:zh=天安门 为例:
OSM name:zh=天安门
│
▼
PlaceSanitizer → tag-analyzer-by-language
├─ 检测后缀 'zh' → 不在 whitelist 中,不创建特定 analyzer
│ (但 use-defaults: all 会为中国境内要素创建 analyzer:zh 变体)
└─ 输出清洗后的名称
│
▼
ICUNameAnalyzer.process_place()
├─ get_analyzer('zh') → 无 'zh' 分析器 → 回退到 generic 分析器
│
├─ generic.get_canonical_id("天安门")
│ → self.norm.transliterate("天安门")
│ → 规范化规则处理(CJK 保留,因为 ICU 的 [:alnum:] 是 Unicode 感知的)
│ → 返回 "天安门"(保持不变)
│
├─ generic.compute_variants("天安门")
│ ├─ _generate_word_variants("天安门") → 无替换规则匹配 → 原样返回
│ └─ self.to_ascii.transliterate("天安门")
│ → 应用音译规则:
│ ① :: Latin () → "tian an men" (ICU 拉丁音译,CJK→拼音)
│ ② extended-unicode-to-asccii.yaml → 无匹配
│ ③ :: Ascii () → "tian an men"
│ ④ [^a-z0-9[:Space:]] > → 移除非法字符
│ ⑤ 最终结果: ["tian an men"]
│
├─ 写入数据库 word 表:
│ - word_token = "tian" (完整词)
│ - word_token = "an" (完整词)
│ - word_token = "men" (完整词)
│ - word_id 关联到原始名称 "天安门"
│
└─ token_info.name_categories 记录:
{"name:zh": "{word_id_123, word_id_456, ...}"}
│
▼
search_name 表:
name_vector: [{word_id_天}, {word_id_安}, {word_id_门}]
token_info: {"names": "{...}", "name_categories": {"name:zh": "{...}"}}
二、查询阶段(Searching)—— 用户输入中文如何匹配
以用户搜索 "天安门" 为例:
用户输入 "天安门"
│
▼
QueryPreprocessor
└─ split_japanese_phrases → 不匹配,原样通过
│
▼
ICUQueryAnalyzer.analyze_query()
│
├─ ① normalize_text("天安门")
│ → self.normalizer.transliterate("天安门")
│ → 应用规范化规则:
│ ① :: lower() → "天安门" (CJK 无大小写)
│ ② :: Hans-Hant → 简繁统一(如天→天)
│ ③ [^[:alnum:]...] > → 保留 CJK(ICU 的 [:alnum:] 含 Unicode 字母)
│ ④ 最终: "天安门"(保留完整)
│
├─ ② split_query("天安门")
│ → re.split('([ :-])', "天安门")
│ → 无分隔符 → 整个字符串作为一个 word
│ → self.transliterator.transliterate("天安门")
│ · :: Latin () → "tian an men" (ICU 拉丁音译器,CJK→拼音)
│ · :: Ascii () → "tian an men"
│ · NFD → lower → [^a-z0-9[:Space:]] > → "tian an men "
│ → 返回 "tian an men"
│ → split_transliteration("tian an men", "天安门")
│ · 按空格拆分成: ["tian", "an", "men"]
│ · 与原始字对齐:
│ ("tian", "天"), ("an", "安"), ("men", "门")
│ → 每个子词作为一个查询节点:
│ Node(btype=BREAK_TOKEN, term_lookup="tian", term_normalized="天")
│ Node(btype=BREAK_TOKEN, term_lookup="an", term_normalized="安")
│ Node(btype=BREAK_TOKEN, term_lookup="men", term_normalized="门")
│
├─ ③ lookup_in_db(["tian", "an", "men"])
│ → SELECT word_id FROM word WHERE word_token IN ('tian','an','men')
│ → 匹配到导入时创建的词条
│
└─ ④ 构建搜索,执行 SQL 检索
→ 生成的 token 与 search_name.name_vector 进行匹配
→ 找到包含这些 token 的地点
│
▼
结果处理
├─ add_result_details() → 补充地址详情
├─ 如果 output_withevidence=True:
│ → compute_match_types()
│ → 从 token_info.name_categories 查出匹配的类别是 "name:zh"
│ → result.match_type = "name:zh"
│ → result.matched_name_tag = "name:zh"
│ → result.matched_name = "天安门"
└─ rerank → sort → 返回
三、关键设计要点
| 环节 | 说明 |
|---|---|
| CJK 字符保留 | ICU 的 [:alnum:] 是 Unicode 感知的,包含 CJK 表意文字(Category Lo),不会在规范化阶段被移除 |
| 简繁统一 | :: Hans-Hant 规则将简体/繁体中文统一,搜索 “天安門” 也会命中 “天安门” |
| 拼音桥接 | :: Latin () 将 CJK 转为拼音,这是中文与英文搜索能共用同一套 token 系统的关键 |
| 无中文分词 | 没有单独的中文分词器(如 jieba),依赖 ICU Latin 音译输出的空格来分割词语 |
| 无中文分析器 | 36 种语言有变体规则(variants),中文没有;中文名称走 generic 分析器 |
| 类别追踪 | name_categories 记录 name:zh 类别,withevidence 功能可以利用它报告匹配的标签类型 |
四、局限性
| 问题 | 影响 |
|---|---|
| 无中文分词 | “南京市长江大桥” 可能被音译为 “nan jing shi chang jiang da qiao”,但无法区分”南京/市长/江大桥” vs “南京市/长江大桥” 的语义差异 |
| 同音字歧义 | 拼音到 token 的转换会丢失声调信息,”山西” (Shanxi) 和 “陕西” (Shaanxi) 可能混淆 |
| 多音字 | “重庆” → “zhong qing” 而非 “chong qing”,因为 ICU Latin 音译器对多音字的处理不一定准确 |
| 无中文变体 | 没有 zh 的 variants 配置,意味着没有中文同义词/缩写扩展(如 “京”↔”北京”) |
五、总结流程图
flowchart TD
subgraph 导入侧
A1["name:zh=天安门"] --> A2["generic.get_canonical_id()"]
A2 --> A3["保留 CJK"]
A3 --> A4["compute_variants()→::Latin→拼音"]
A4 --> A5["word 表: tian/an/men"]
A4 --> A6["name_categories: name:zh→{id}"]
end
subgraph 查询侧
B1["用户输入 '天安门'"] --> B2["normalize_text()"]
B2 --> B3["保留 CJK"]
B3 --> B4["split_query()→::Latin→'tian an men'"]
B4 --> B5["split_transliteration→(tian/an/men)"]
B5 --> B6["lookup_in_db('tian','an','men')"]
B6 --> B7["匹配 word 表→取 word_id"]
end
A5 --> B6
B7 --> C1["SQL: name_vector 匹配→返回结果"]
C1 --> C2["compute_match_types→match_type='name:zh'"]
核心机制:导入时与查询时走完全相同的音译管道(normalize → :: Latin → ASCII → 分词),确保拼音 token 两侧一致,从而实现中文搜索。这本质上是一个拼音全文检索方案,而非真正的语义中文搜索。