跳转到内容

模組:Category tree/topic/Places

維基詞典,自由的多語言詞典


-- Chinese Wiktionary port of enwikt [[Module:category tree/topic/Places]].
-- Generates descriptions and parent-category specs for place-related category pages.
--
-- Architecture mirrors enwikt: two tables exported as LABELS and HANDLERS.
--   LABELS   — static label → spec mappings (top-level grouping categories)
--   HANDLERS — ordered list of functions; first non-nil return wins
--
-- Chinese-specific adaptations vs. enwikt:
--   * Pattern matching on 的 instead of English "in"/"of"
--   * Handler 3 bare-location parent: CONTAINER的TYPE (order reversed vs. enwikt)
--     Special case: TYPE=="地點" → CONTAINERTYPE (no 的)
--   * Handler 6 uses forward zh_name lookup (div.type → zh_name) instead of
--     a reverse map, because many English placetypes share the same Chinese name
--     (e.g. state/oblast/canton all → "州"; province/department/voivodeship → "省")
--   * Handler 7 (new): compound capital label + location ("美國的州首府")
--     Compound = zh_name(holonym_type) .. zh_cap_label(holonym_type)
--     Built at load time; skips country/constituent country (no 國首都 category)
--   * No article handling (Chinese has no definite article)
--   * Serial enumeration: 、(non-final) 和(final)
--   * Description format: {{{langname}}}中LOCATION_DESC[[PLACETYPE]]的名稱。

local labels   = {}
local handlers = {}

local m_table      = require("Module:table")
local m_locations  = require("Module:place/locations")
local m_placetypes = require("Module:place/placetypes")
local m_zh_data    = require("Module:place/data")

local internal_error        = m_locations.internal_error
local zh_strings            = m_placetypes.zh_strings
local zh_cap_by_holonym     = m_placetypes.zh_capital_label_by_holonym_type
local format_zh_name        = m_placetypes.format_zh_name

local insert = table.insert
local concat = table.concat

local function is_callable(val) return type(val) == "function" end

-- Returns 的 for 1-char zh_type strings; "" for 2+ chars.
-- Mirrors export.zh_cat_de in place-placetypes.lua but takes an already-formatted string.
local function zh_de(zh_type)
	if not zh_type then return "" end
	if mw.ustring.len(zh_type) == 1 then
		return zh_strings.de_particle
	end
	if mw.ustring.match(zh_type, "^.和") then
		return zh_strings.de_particle
	end
	return ""
end

-- Append candidate splits for 2+ char zh_names used as bare suffixes (no 的 form).
-- name_map: any table whose keys are zh_name strings (zh_name_to_pt_keys, etc.).
-- Skips suffixes whose preceding text ends in 的 (already captured by split_at_de).
local function add_no_de_suffix_splits(splits, label, name_map)
	local de_bytes = #zh_strings.de_particle
	for zh_type in pairs(name_map) do
		if mw.ustring.len(zh_type) >= 2 then
			local bytes = #zh_type
			if #label > bytes and label:sub(-bytes) == zh_type then
				local place_str = label:sub(1, #label - bytes)
				if place_str:sub(-de_bytes) ~= zh_strings.de_particle then
					insert(splits, {place = place_str, placetype = zh_type})
				end
			end
		end
	end
end

-- Literal-string gsub (no regex metacharacters interpreted in `from` or `to`).
local function gsub_literally(str, from, to)
	return (str:gsub(from:gsub("[%(%)%.%%%+%-%*%?%[%^%$]", "%%%1"),
	                 to:gsub("%%", "%%%%")))
end

-- Get the first (primary) placetype from spec.
local function fetch_primary_placetype(key, spec)
	local placetype = spec.placetype
	if type(placetype) == "table" then placetype = placetype[1] end
	return placetype
end

-- True for continent/supercontinent/planet specs (used to omit 的 in 非洲國家).
local function spec_is_broad(spec)
	local pt = spec and spec.placetype
	if not pt then return false end
	local pts = type(pt) == "table" and pt or {pt}
	for _, p in ipairs(pts) do
		if p == "continent" or p == "supercontinent" or p == "planet"
		   or p == "continental region" then return true end
	end
	return false
end

-- ---------------------------------------------------------------------------
-- Chinese serial joiner: {"A","B","C"} → "A、B和C"
-- ---------------------------------------------------------------------------
local function zh_join(items)
	local n = #items
	if n == 0 then return "" end
	if n == 1 then return items[1] end
	local parts = {}
	for i = 1, n - 1 do
		insert(parts, items[i])
		if i < n - 1 then insert(parts, zh_strings.enum_sep) end
	end
	insert(parts, zh_strings.and_conj)
	insert(parts, items[n])
	return concat(parts)
end

-- ---------------------------------------------------------------------------
-- Placetype class → Chinese bare parent category label
-- ---------------------------------------------------------------------------
local class_to_bare_category_parent = {
	["polity"]               = "政治實體",
	["subpolity"]            = "政治區劃",
	["settlement"]           = "聚居地",
	["non-admin settlement"] = "聚居地",
	["capital"]              = "首都",
	["natural feature"]      = "自然",
	["man-made structure"]   = "人造構築物",
	["geographic region"]    = "地理文化區域",
}

local class_is_political_division = {
	["polity"]               = true,
	["subpolity"]            = true,
	["settlement"]           = true,
	["non-admin settlement"] = false,
	["capital"]              = true,
	["natural feature"]      = false,
	["man-made structure"]   = false,
	["geographic region"]    = false,
	["generic place"]        = false,
}

-- Enwikt bare_category_parent / addl_bare_category_parents → zhwikt label.
-- false = no equivalent, skip.
local en_category_to_zh = {
	["islands"]              = "島嶼",
	["cities"]               = "城市",
	["political divisions"]  = "政治區劃",
	["former polities"]      = "前政治實體",
	["former places"]        = "地點",
	["places"]               = "地點",
	["former settlements"]   = "歷史聚居地",
	["country-like entities"]= "類國家實體",
	["names"]                = "名稱",
	["man-made structures"]  = "人造構築物",
	["countries"]            = "國家",
	["bodies of water"]      = "水體",
	["water"]                = "水",
	["landforms"]            = "地形",
	["ecosystems"]           = "生態系統",
	["forestry"]             = "林學",
	["mountains"]            = "山",
	["seas"]                 = "海",
	["bridges"]              = "橋",
	["buildings"]            = "建築",
	["roads"]                = "道路",
}

-- Translate enwikt category parent string to zhwikt equivalent.
-- Returns zh string, or nil if should be skipped.
-- Non-English strings (assumed already Chinese) pass through unchanged.
local function translate_cat_parent(val)
	if type(val) ~= "string" then return nil end
	local mapped = en_category_to_zh[val]
	if mapped == false then return nil end
	if mapped then return mapped end
	return val
end

-- ---------------------------------------------------------------------------
-- Load-time reverse maps
-- ---------------------------------------------------------------------------

-- Chinese capital label → holonym type  (e.g. "首都" → "country")
local zh_cap_label_to_holonym = {}
for holonym_type, zh_label in pairs(zh_cap_by_holonym) do
	if not zh_cap_label_to_holonym[zh_label] then
		zh_cap_label_to_holonym[zh_label] = holonym_type
	end
end

-- Chinese placetype name → list of English placetype keys (one-to-many).
-- Used by handler 2 (bare placetype) and handler 4 (generic X的Y).
local zh_name_to_pt_keys = {}
do
	local function add(name, en_key)
		zh_name_to_pt_keys[name] = zh_name_to_pt_keys[name] or {}
		insert(zh_name_to_pt_keys[name], en_key)
	end
	for en_key, entry in pairs(m_zh_data.zh_placetype_data) do
		if entry.zh_name then
			local names = type(entry.zh_name) == "table" and entry.zh_name or {entry.zh_name}
			for _, name in ipairs(names) do add(name, en_key) end
		end
		if entry.zh_name_by_holonym_type then
			for _, zh_name_val in pairs(entry.zh_name_by_holonym_type) do
				local names = type(zh_name_val) == "table" and zh_name_val or {zh_name_val}
				for _, name in ipairs(names) do add(name, en_key) end
			end
		end
	end
end

-- Compound capital label → list of holonym placetype keys.
-- Compound = zh_name(holonym_type) .. zh_cap_label(holonym_type).
-- E.g. "州首府" → {"state"}, "地區首府" → {"territory", "region"}.
-- Skip country/constituent country (no 國首都 category per design).
-- Skip entries where compound == cap_label (e.g. "省省會"/"縣縣治" excluded to
-- avoid ambiguous matches — these bare labels are already handled by handler 5).
local zh_compound_cap_to_placetypes = {}
do
	local function add_compound(compound, pt)
		zh_compound_cap_to_placetypes[compound] = zh_compound_cap_to_placetypes[compound] or {}
		insert(zh_compound_cap_to_placetypes[compound], pt)
	end
	for holonym_type, cap_label in pairs(zh_cap_by_holonym) do
		if holonym_type ~= "country" and holonym_type ~= "constituent country" then
			local zh_name_raw = m_placetypes.get_zh_placetype_name(holonym_type, nil)
			if zh_name_raw then
				local zh_name_str = format_zh_name(zh_name_raw)
				local compound = zh_name_str .. cap_label
				if compound ~= cap_label then
					add_compound(compound, holonym_type)
				end
			end
		end
	end
end

-- ---------------------------------------------------------------------------
-- Location-linking helpers
-- ---------------------------------------------------------------------------

local function construct_linked_location(group, key, spec)
	local full_placename = m_locations.key_to_placename(group, key)
	return m_locations.construct_linked_placename(spec, full_placename)
end

-- Returns a location description string for embedding in category descriptions.
-- Shows one level of container for sub-national entries:
--   country   → [[法國]]
--   state     → [[加州]]([[美國]]的[[州]])
--   city      → [[舊金山]]([[加州]]的[[城市]])
local function construct_location_desc(group, key, spec)
	local linked = construct_linked_location(group, key, spec)
	if spec.no_include_container_in_desc then return linked end
	local container_iterator = m_locations.iterate_containers(group, key, spec)
	local containers = container_iterator()
	if not containers then return linked end

	local location_type = spec.placetype
	if type(location_type) == "table" then location_type = location_type[1] end
	local zh_name_raw = location_type and m_placetypes.get_zh_placetype_name(location_type, nil)
	local zh_type = zh_name_raw and format_zh_name(zh_name_raw)

	local cont_links = {}
	for _, c in ipairs(containers) do
		insert(cont_links, construct_linked_location(c.group, c.key, c.spec))
	end
	local cont_str = zh_join(cont_links)

	if zh_type then
		return linked .. "(" .. cont_str .. "的" .. zh_type .. ")"
	else
		return linked .. "(位於" .. cont_str .. ")"
	end
end

-- Fetch or construct the location description, substituting "+++" with the
-- auto-constructed description if present in spec.keydesc.
local function fetch_or_construct_location_desc(group, key, spec)
	local val = spec.keydesc
	if is_callable(val) then
		val = val(group, key, spec)
		spec.keydesc = val
	end
	if val then
		if val:find("%+%+%+", 1, true) then
			local base = construct_location_desc(group, key, spec)
			val = gsub_literally(val, "+++", base)
		end
		return val
	end
	return construct_location_desc(group, key, spec)
end

-- ---------------------------------------------------------------------------
-- Split label at every occurrence of 的
-- Returns list of {place, placetype} tables (all possible splits left→right).
-- ---------------------------------------------------------------------------
local function split_at_de(label)
	local results = {}
	local de = "的"
	local start = 1
	while true do
		local s, e = label:find(de, start, true)
		if not s then break end
		local place_part = label:sub(1, s - 1)
		local type_part  = label:sub(e + 1)
		if #place_part > 0 and #type_part > 0 then
			insert(results, {place = place_part, placetype = type_part})
		end
		start = e + 1
	end
	return results
end

-- Locate group + key + spec for a Chinese location string.
local function find_place(place_str)
	local group, spec = m_locations.find_canonical_key(place_str)
	if group then return group, place_str, spec end
	return nil
end

-- No articles in Chinese; just return the key.
local function get_prefixed_key(key, spec)
	if m_placetypes.get_prefixed_key then
		return m_placetypes.get_prefixed_key(key, spec)
	end
	return key
end

-- Check whether zh_name_val (string or array) contains type_zh.
local function zh_name_has(zh_name_val, type_zh)
	if type(zh_name_val) == "table" then
		for _, n in ipairs(zh_name_val) do
			if n == type_zh then return true end
		end
		return false
	end
	return zh_name_val == type_zh
end

-- Search divs list for a div whose Chinese name matches type_zh.
-- Singularizes div.type before zh_name lookup (divs use plural English keys
-- like "states", while zh_placetype_data uses singular keys like "state").
-- Uses forward lookup (English key → zh_name) to handle many-to-one collisions.
-- Returns: div_parent (English key or false), prep, matched_en_type.
-- div_parent == nil means no match; div_parent == false means match but no parent cat.
-- holonym_pt: the primary placetype of the container spec (e.g. "empire" for Roman Empire).
-- Passed to get_zh_placetype_name so context-sensitive zh_name_by_holonym_type overrides apply
-- (e.g. "province" + holonym_pt "empire" → "行省" instead of "省").
local function find_div_by_zh_name(divs, type_zh, holonym_pt)
	if not divs then return nil, nil, nil end
	if type(divs) ~= "table" then divs = {divs} end
	for _, div in ipairs(divs) do
		if type(div) == "string" then div = {type = div} end
		local cat_as = div.cat_as or div.type
		if type(cat_as) ~= "table" or cat_as.type then cat_as = {cat_as} end
		for _, pt_cat_as in ipairs(cat_as) do
			if type(pt_cat_as) == "string" then pt_cat_as = {type = pt_cat_as} end
			-- Try direct key first (handles combined keys like "provinces and autonomous regions"),
			-- then singularize as fallback (handles standard plural keys like "provinces").
			local lookup_type = m_placetypes.maybe_singularize_placetype(pt_cat_as.type)
				or pt_cat_as.type
			local div_zh_raw = m_placetypes.get_zh_placetype_name(pt_cat_as.type, holonym_pt)
				or m_placetypes.get_zh_placetype_name(lookup_type, holonym_pt)
			if div_zh_raw and zh_name_has(div_zh_raw, type_zh) then
				local div_parent = pt_cat_as.container_parent_type
				if div_parent == nil then div_parent = div.container_parent_type end
				if div_parent == nil then div_parent = pt_cat_as.type end
				return div_parent, pt_cat_as.prep or div.prep or "of", lookup_type
			end
		end
	end
	return nil, nil, nil
end

-- Look up Chinese category name from a container's div cat_as, mirroring enwikt's
-- PL_PLACETYPE resolution. Used by handler 3 so that provinces + autonomous regions
-- of China both resolve to "省和自治區" rather than their individual zh_names.
-- Returns zh_name string (e.g. "省和自治區"), or nil to fall back to parent_zh_type.
local function find_cat_zh_from_container_divs(loc_type, container_spec)
	if not loc_type or not container_spec then return nil end
	local pl_type = m_placetypes.pluralize_placetype(loc_type)
	if not pl_type then return nil end
	for _, div_list in ipairs({container_spec.divs, container_spec.addl_divs}) do
		if div_list then
			local divs = type(div_list) == "table" and div_list or {div_list}
			for _, div in ipairs(divs) do
				if type(div) == "string" then div = {type = div} end
				if div.type == pl_type then
					local cat_as = div.cat_as
					if cat_as and type(cat_as) == "string" then
						-- Try cat_as directly first (handles combined keys like
						-- "provinces and autonomous regions" which singularize incorrectly).
						local zh_raw = m_placetypes.get_zh_placetype_name(cat_as, nil)
						if not zh_raw then
							local sg = m_placetypes.maybe_singularize_placetype(cat_as)
							if sg then zh_raw = m_placetypes.get_zh_placetype_name(sg, nil) end
						end
						if zh_raw then return format_zh_name(zh_raw) end
					end
					return nil  -- found div but no usable cat_as
				end
			end
		end
	end
	return nil
end

-- ===========================================================================
-- Handler 1 — Bare capital category labels
-- e.g. "首都", "省會", "首府", "縣治"
-- ===========================================================================
insert(handlers, function(label)
	local holonym_type = zh_cap_label_to_holonym[label]
	if not holonym_type then return nil end
	local zh_raw = m_placetypes.get_zh_placetype_name(holonym_type, nil)
	local zh_holonym = zh_raw and format_zh_name(zh_raw) or holonym_type
	return {
		type = "name",
		topic = label,
		description = "{{{langname}}}中" .. zh_holonym .. "[[" .. label .. "]]的名稱。",
		parents = {"首都"},
	}
end)

-- ===========================================================================
-- Handler 2 — Bare placetype categories
-- e.g. "城市", "河流", "山脈", "國家"
-- ===========================================================================
insert(handlers, function(label)
	local en_keys = zh_name_to_pt_keys[label]
	if not en_keys then return nil end
	for _, en_key in ipairs(en_keys) do
		local canon_pt, ptdata = m_placetypes.get_placetype_data(en_key, "from category")
		if canon_pt then
			local from_cat = {from_category = true, no_split_qualifiers = true}
			local bare_parent = m_placetypes.get_equiv_placetype_prop(canon_pt, function(pt)
				local bcp = m_placetypes.get_placetype_prop(pt, "bare_category_parent")
				if bcp then
					local zh = translate_cat_parent(bcp)
					if zh then return zh end
				end
				local cls = m_placetypes.get_placetype_prop(pt, "class")
				if cls then return class_to_bare_category_parent[cls] end
			end, from_cat) or "地點"
			local addl_parents_raw = m_placetypes.get_equiv_placetype_prop(canon_pt, function(pt)
				return m_placetypes.get_placetype_prop(pt, "addl_bare_category_parents")
			end, from_cat)
			local addl_parents
			if addl_parents_raw then
				addl_parents = {}
				for _, v in ipairs(addl_parents_raw) do
					local zh = translate_cat_parent(v)
					if zh then m_table.insertIfNot(addl_parents, zh) end
				end
				if #addl_parents == 0 then addl_parents = nil end
			end
			local breadcrumb = m_placetypes.get_equiv_placetype_prop(canon_pt, function(pt)
				return m_placetypes.get_placetype_prop(pt, "bare_category_breadcrumb")
			end, from_cat)
			if type(bare_parent) == "string" and breadcrumb then
				bare_parent = {name = bare_parent, sort = breadcrumb}
			end
			local parents = {bare_parent}
			if addl_parents then m_table.extend(parents, addl_parents) end
			return {
				type = "name",
				topic = label,
				description = "{{{langname}}}中[[" .. label .. "]]的名稱。",
				breadcrumb = breadcrumb,
				parents = parents,
			}
		end
	end
end)

-- ===========================================================================
-- Handler 3 — Bare placename categories (known locations)
-- e.g. "美國", "法國", "北京", "加利福尼亞州"
-- Supports: overriding_bare_label_parents, bare_category_parent_type,
--           "+++" expansion, addl_parents, wp/wpcat/commonscat, fulldesc.
-- "+++" expands to: CONTAINER的TYPE (Chinese order) or CONTAINERTYPE for "地點".
-- ===========================================================================
insert(handlers, function(label)
	local group, spec = m_locations.find_canonical_key(label)
	if not group then return nil end

	local wp = spec.wp
	if wp == nil then wp = true end
	local wpcat = spec.wpcat
	if wpcat == nil then wpcat = wp end
	local commonscat = spec.commonscat
	if commonscat == nil then commonscat = wpcat end

	local full_placename = m_locations.key_to_placename(group, label)
	local full_container_placename

	local container_iterator = m_locations.iterate_containers(group, label, spec)
	local containers = container_iterator()
	if containers then
		full_container_placename = m_locations.key_to_placename(
			containers[1].group, containers[1].key)
	end

	-- Substitute %l (full placename), %e (same, no elliptical form in Chinese),
	-- %c (container placename) in wp/wpcat/commonscat values.
	local function format_boxval(val)
		if val == true then val = "%l" end
		if type(val) == "string" then
			val = gsub_literally(val, "%l", full_placename)
			val = gsub_literally(val, "%e", full_placename)
			if val:find("%%c", 1, true) and full_container_placename then
				val = gsub_literally(val, "%c", full_container_placename)
			end
		end
		return val
	end

	-- Determine Chinese type label for parent category construction.
	-- bare_category_parent_type overrides the location's own placetype.
	local parent_zh_type
	do
		local override = spec.bare_category_parent_type
		if override then
			local t = type(override) == "table"
				and (override.type or override[1]) or override
			local raw = type(t) == "string"
				and m_placetypes.get_zh_placetype_name(t, nil)
			parent_zh_type = raw and format_zh_name(raw)
		end
		if not parent_zh_type then
			local loc_type = fetch_primary_placetype(label, spec)
			local raw = loc_type and m_placetypes.get_zh_placetype_name(loc_type, nil)
			parent_zh_type = raw and format_zh_name(raw)
		end
	end

	local parents = {}
	local bare_label_parents = spec.overriding_bare_label_parents
	if not bare_label_parents then
		bare_label_parents = {"+++"}
	end

	local inserted_containers = false
	local loc_type = fetch_primary_placetype(label, spec)
	for _, parent in ipairs(bare_label_parents) do
		if parent == "+++" then
			-- Expand to Chinese parent: CONTAINER的TYPE or CONTAINERTYPE for "地點";
			-- omit 的 when TYPE=="國家" and container is broad (大洲/大陸地區 → 亞洲國家/加勒比地區國家);
			-- use bare container when TYPE=="國家" but container is not broad (新西蘭 not 新西蘭的國家).
			-- First, try container's div cat_as (mirrors enwikt PL_PLACETYPE resolution),
			-- so that e.g. provinces and ARs of China both resolve to "省和自治區".
			if containers then
				for _, container in ipairs(containers) do
					local ck = get_prefixed_key(container.key, container.spec)
					local effective_zh_type = (loc_type
						and find_cat_zh_from_container_divs(loc_type, container.spec))
						or parent_zh_type
					local parent_cat
					if effective_zh_type == "地點" then
						parent_cat = ck .. "地點"
					elseif effective_zh_type == "國家" then
						if spec_is_broad(container.spec) then
							parent_cat = ck .. "國家"
						else
							parent_cat = ck
						end
					elseif effective_zh_type then
						parent_cat = ck .. zh_de(effective_zh_type) .. effective_zh_type
					else
						parent_cat = ck
					end
					m_table.insertIfNot(parents, parent_cat)
				end
				inserted_containers = true
			end
		else
			m_table.insertIfNot(parents, parent)
		end
	end
	-- If "+++" wasn't present in bare_label_parents and containers exist, insert
	-- bare container keys as fallback (mirrors enwikt behaviour).
	if not inserted_containers and containers then
		for _, container in ipairs(containers) do
			m_table.insertIfNot(parents, container.key)
		end
	end

	if spec.addl_parents then
		for _, p in ipairs(spec.addl_parents) do
			m_table.insertIfNot(parents, p)
		end
	end
	if #parents == 0 then insert(parents, "地點") end

	local description = spec.fulldesc or (
		"{{{langname}}}中與" ..
		fetch_or_construct_location_desc(group, label, spec) ..
		"相關的詞語。")

	return {
		type = "topic",
		description = description,
		breadcrumb = full_placename,
		parents = parents,
		wp      = format_boxval(wp),
		wpcat   = format_boxval(wpcat),
		commonscat = format_boxval(commonscat),
	}
end)

-- ===========================================================================
-- Handler 4 — Generic placetype + location
-- e.g. "法國的城市", "德國的河流", "亞洲的國家"
-- Matches placetypes that carry generic_before_non_cities or generic_before_cities.
-- ===========================================================================
insert(handlers, function(label)
	local splits = split_at_de(label)
	add_no_de_suffix_splits(splits, label, zh_name_to_pt_keys)
	for _, split in ipairs(splits) do
		local place_str = split.place
		local type_zh   = split.placetype
		local en_keys = zh_name_to_pt_keys[type_zh]
		if en_keys then
			for _, en_key in ipairs(en_keys) do
				local canon_pt, ptdata = m_placetypes.get_placetype_data(en_key, "from category")
				if canon_pt and ptdata and
				   (ptdata.generic_before_non_cities or ptdata.generic_before_cities) then
					local group, key, spec = find_place(place_str)
					if group then
						local allow = true
						if spec.is_former_place and en_key ~= "place" then allow = false end
						if allow then
							local loc_desc = fetch_or_construct_location_desc(group, key, spec)
							local desc = "{{{langname}}}中" .. loc_desc .. "[[" .. type_zh .. "]]的名稱。"
							local parents = {key}
							if spec.no_container_parent then
								insert(parents, {name = type_zh, sort = key})
								local spt = spec.placetype
								if spt == "country" or (type(spt) == "table" and m_table.contains(spt, "country")) then
									insert(parents, "特定國家的政治區劃")
								end
							else
								local cont_iter = m_locations.iterate_containers(group, key, spec)
								local next_conts = cont_iter()
								if next_conts then
									for _, c in ipairs(next_conts) do
										insert(parents, {
											name = get_prefixed_key(c.key, c.spec) .. zh_de(type_zh) .. type_zh,
											sort = key
										})
									end
								else
									insert(parents, {name = type_zh, sort = key})
								end
							end
							return {
								type = "name",
								topic = label,
								description = desc,
								breadcrumb = type_zh,
								parents = parents,
							}
						end
					end
				end
			end
		end
	end
end)

-- ===========================================================================
-- Handler 4b — Continent/broad location + 國家 (no 的)
-- e.g. "非洲國家", "亞洲國家"
-- Handles the no-的 form generated for country categories under continents.
-- ===========================================================================
insert(handlers, function(label)
	local zh_type = "國家"
	local zh_type_len = #zh_type  -- byte length (6 for 2 Chinese chars in UTF-8)
	if #label <= zh_type_len or label:sub(-zh_type_len) ~= zh_type then return nil end
	local place_str = label:sub(1, #label - zh_type_len)
	local group, key, spec = find_place(place_str)
	if not group or not spec_is_broad(spec) then return nil end
	local loc_desc = fetch_or_construct_location_desc(group, key, spec)
	local desc = "{{{langname}}}中" .. loc_desc .. "[[" .. zh_type .. "]]的名稱。"
	local parents = {key, {name = zh_type, sort = " " .. key}}
	return {
		type = "name",
		topic = label,
		description = desc,
		breadcrumb = zh_type,
		parents = parents,
	}
end)

-- ===========================================================================
-- Handler 4c — Generic "X地點" categories (no 的)
-- e.g. "臺灣地點", "美國地點", "加州地點"
-- generic_place_cat_handler in place-placetypes.lua emits this no-的 form.
-- Handler 4 (split_at_de) cannot match it because there is no 的 separator.
-- ===========================================================================
insert(handlers, function(label)
	local zh_type = "地點"
	local zh_type_len = #zh_type  -- 6 bytes (2 UTF-8 chars)
	if #label <= zh_type_len or label:sub(-zh_type_len) ~= zh_type then return nil end
	local place_str = label:sub(1, #label - zh_type_len)
	local group, key, spec = find_place(place_str)
	if not group then return nil end
	local loc_desc = fetch_or_construct_location_desc(group, key, spec)
	local desc = "{{{langname}}}中" .. loc_desc .. "[[" .. zh_type .. "]]的名稱。"
	local parents = {key}
	if spec.no_container_parent then
		insert(parents, {name = zh_type, sort = " " .. key})
	else
		local cont_iter = m_locations.iterate_containers(group, key, spec)
		local next_conts = cont_iter()
		if next_conts then
			for _, c in ipairs(next_conts) do
				insert(parents, {
					name = get_prefixed_key(c.key, c.spec) .. zh_type,
					sort = key
				})
			end
		else
			insert(parents, {name = zh_type, sort = " " .. key})
		end
	end
	return {
		type = "name",
		topic = label,
		description = desc,
		breadcrumb = zh_type,
		parents = parents,
	}
end)

-- ===========================================================================
-- Handler 5 — Capital + location (bare capital label)
-- e.g. "美國的首都", "四川省的省會", "德克薩斯州的首府"
-- Handles bare capital labels from zh_cap_label_to_holonym.
-- Must precede handler 6 (compound capital) and handler 7 (division) to avoid
-- mis-matching capital labels that also appear in a location's divs via cat_as.
-- ===========================================================================
insert(handlers, function(label)
	local splits = split_at_de(label)
	add_no_de_suffix_splits(splits, label, zh_cap_label_to_holonym)
	for _, split in ipairs(splits) do
		local place_str = split.place
		local type_zh   = split.placetype
		if zh_cap_label_to_holonym[type_zh] then
			local group, key, spec = find_place(place_str)
			if group then
				local loc_desc = fetch_or_construct_location_desc(group, key, spec)
				local desc = "{{{langname}}}中" .. loc_desc .. "[[" .. type_zh .. "]]的名稱。"
				local parents = {key}
				if spec.no_container_parent then
					insert(parents, {name = type_zh, sort = key})
				else
					local cont_iter = m_locations.iterate_containers(group, key, spec)
					local next_conts = cont_iter()
					if next_conts then
						for _, c in ipairs(next_conts) do
							insert(parents, {
								name = get_prefixed_key(c.key, c.spec) .. zh_de(type_zh) .. type_zh,
								sort = key
							})
						end
					else
						insert(parents, {name = type_zh, sort = key})
					end
				end
				return {
					type = "name",
					topic = label,
					description = desc,
					breadcrumb = type_zh,
					parents = parents,
				}
			end
		end
	end
end)

-- ===========================================================================
-- Handler 6 — Compound capital label + location
-- e.g. "美國的州首府", "法國的行政區首府", "澳大利亞的地區首府"
-- Compound label = zh_name(holonym_type) .. zh_cap_label(holonym_type).
-- Built at load time in zh_compound_cap_to_placetypes.
-- Must precede handler 7 (division) to avoid mis-matching.
-- ===========================================================================
insert(handlers, function(label)
	local splits = split_at_de(label)
	add_no_de_suffix_splits(splits, label, zh_compound_cap_to_placetypes)
	for _, split in ipairs(splits) do
		local place_str = split.place
		local type_zh   = split.placetype
		local target_pts = zh_compound_cap_to_placetypes[type_zh]
		if target_pts then
			local group, key, spec = find_place(place_str)
			if group then
				-- Check if any target placetype appears in this location's divs.
				-- Div types are plural English strings ("states"); singularize before comparing.
				local found = false
				for _, divlist in ipairs({spec.divs, spec.addl_divs,
				                          spec.addl_divs_for_categorization}) do
					if divlist and not found then
						if type(divlist) ~= "table" then divlist = {divlist} end
						for _, div in ipairs(divlist) do
							if type(div) == "string" then div = {type = div} end
							local function check_div_type(pt_type)
								local sg = m_placetypes.maybe_singularize_placetype(pt_type)
									or pt_type
								for _, target_pt in ipairs(target_pts) do
									if sg == target_pt or pt_type == target_pt then
										found = true
										break
									end
								end
							end
							check_div_type(div.type)
							if not found and div.cat_as then
								local ca = div.cat_as
								if type(ca) ~= "table" or ca.type then ca = {ca} end
								for _, pt_ca in ipairs(ca) do
									if type(pt_ca) == "string" then
										pt_ca = {type = pt_ca}
									end
									check_div_type(pt_ca.type)
									if found then break end
								end
							end
							if found then break end
						end
					end
					if found then break end
				end

				if found then
					local loc_desc = fetch_or_construct_location_desc(group, key, spec)
					local desc = "{{{langname}}}中" .. loc_desc
						.. "[[" .. type_zh .. "]]的名稱。"
					local parents = {key}
					if spec.no_container_parent then
						insert(parents, {name = type_zh, sort = key})
						local spt = spec.placetype
						if spt == "country" or
						   (type(spt) == "table" and m_table.contains(spt, "country")) then
							insert(parents, "特定國家的政治區劃")
						end
					else
						local cont_iter = m_locations.iterate_containers(group, key, spec)
						local next_conts = cont_iter()
						if next_conts then
							for _, c in ipairs(next_conts) do
								insert(parents, {
									name = get_prefixed_key(c.key, c.spec) .. zh_de(type_zh) .. type_zh,
									sort = key
								})
							end
						else
							insert(parents, {name = type_zh, sort = key})
						end
					end
					return {
						type = "name",
						topic = label,
						description = desc,
						breadcrumb = type_zh,
						parents = parents,
					}
				end
			end
		end
	end
end)

-- ===========================================================================
-- Handler 7 — Administrative division + location
-- e.g. "美國的州", "法國的省", "英國的構成國", "日本的縣"
-- Uses forward zh_name lookup (English div type → zh_name) to avoid collision
-- between English types that share the same Chinese name (state/oblast/canton → "州").
-- Singularizes plural div types ("states" → "state") before zh_name lookup.
-- ===========================================================================
insert(handlers, function(label)
	local splits = split_at_de(label)
	add_no_de_suffix_splits(splits, label, zh_name_to_pt_keys)
	for _, split in ipairs(splits) do
		local place_str = split.place
		local type_zh   = split.placetype
		local group, key, spec = find_place(place_str)
		if group then
			local primary_pt = fetch_primary_placetype(key, spec)
			local div_parent, div_prep, matched_type = find_div_by_zh_name(spec.divs, type_zh, primary_pt)
			if div_parent == nil then
				div_parent, div_prep, matched_type = find_div_by_zh_name(spec.addl_divs, type_zh, primary_pt)
			end
			if div_parent == nil then
				div_parent, div_prep, matched_type =
					find_div_by_zh_name(spec.addl_divs_for_categorization, type_zh, primary_pt)
			end
			if div_parent ~= nil then
				local loc_desc = fetch_or_construct_location_desc(group, key, spec)
				local desc = "{{{langname}}}中" .. loc_desc .. "[[" .. type_zh .. "]]的名稱。"
				local parents = {key}
				if div_parent then  -- div_parent == false means suppress parent cat
					if spec.no_container_parent then
						insert(parents, {name = type_zh, sort = " " .. key})
						local spt = spec.placetype
						if spt == "country" or (type(spt) == "table" and m_table.contains(spt, "country")) then
							insert(parents, "特定國家的政治區劃")
						end
					else
						local cont_iter = m_locations.iterate_containers(group, key, spec)
						local next_conts = cont_iter()
						if next_conts then
							for _, c in ipairs(next_conts) do
								-- Map English div_parent key back to Chinese for parent category name.
								local dp_zh
								if type(div_parent) == "string" then
									local dp_raw = m_placetypes.get_zh_placetype_name(div_parent, nil)
									if not dp_raw then
										local sg_dp = m_placetypes.maybe_singularize_placetype(div_parent)
										if sg_dp then dp_raw = m_placetypes.get_zh_placetype_name(sg_dp, nil) end
									end
									dp_zh = dp_raw and format_zh_name(dp_raw) or type_zh
								else
									dp_zh = type_zh
								end
								insert(parents, {
									name = get_prefixed_key(c.key, c.spec) .. zh_de(dp_zh) .. dp_zh,
									sort = key
								})
							end
						else
							insert(parents, {name = type_zh, sort = " " .. key})
						end
					end
				end
				return {
					type = "name",
					topic = label,
					description = desc,
					breadcrumb = type_zh,
					parents = parents,
				}
			end
		end
	end
end)

-- ===========================================================================
-- Static labels — top-level grouping categories
-- ===========================================================================

labels["地點"] = {
	type = "grouping",
	description = "{{{langname}}}中地點的分類。",
	parents = {"名稱"},
}

labels["政治實體"] = {
	type = "grouping",
	description = "{{{langname}}}中政治實體的名稱。",
	parents = {"地點"},
}

labels["類國家實體"] = {
	type = "grouping",
	description = "{{{langname}}}中類國家實體(海外屬地、未被普遍承認之國家等)的名稱。",
	parents = {"政治實體"},
}

labels["聚居地"] = {
	type = "grouping",
	description = "{{{langname}}}中聚落地名。",
	parents = {"地點"},
}

labels["首都"] = {
	type = "grouping",
	description = "{{{langname}}}中首都及各類首府的名稱。",
	parents = {"聚居地"},
}

labels["地形"] = {
	type = "grouping",
	description = "{{{langname}}}中地形的名稱。",
	parents = {"自然"},
}

labels["人造構築物"] = {
	type = "grouping",
	description = "{{{langname}}}中人造構築物的名稱。",
	parents = {"地點"},
}

labels["地理文化區域"] = {
	type = "grouping",
	description = "{{{langname}}}中地理及文化區域的名稱。",
	parents = {"地點"},
}

labels["政治區劃"] = {
	type = "grouping",
	description = "{{{langname}}}中政治區劃(行政區及其他非主權行政分區)的名稱。",
	parents = {"地點"},
}

labels["前政治實體"] = {
	type = "grouping",
	description = "{{{langname}}}中已消亡的政治實體(國家、王國、帝國等)的名稱。",
	parents = {"政治實體"},
}

labels["特定國家的政治區劃"] = {
	type = "grouping",
	description = "{{{langname}}}中特定國家政治區劃的分類。",
	parents = {"地點"},
}

-- ---------------------------------------------------------------------------
-- Misc. place-related labels
-- ---------------------------------------------------------------------------

labels["外名"] = {
	type = "name",
	description = "{{{langname}}}[[外名]]。",
	parents = {"地點"},
}

labels["古埃及行政區"] = {
	type = "name",
	description = "{{{langname}}}中[[古埃及]][[行政區]]的名稱。",
	breadcrumb = "行政區",
	parents = {"古埃及"},
}

-- ---------------------------------------------------------------------------
-- Sui generis place categories (cross-jurisdictional / transcontinental regions)
-- ---------------------------------------------------------------------------

labels["大西洋"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[大西洋]]相關的詞語。",
	parents = {"地球"},
}

labels["不列顛群島"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[大不列顛島]]、[[愛爾蘭島]]及鄰近島嶼相關的詞語。",
	parents = {"歐洲", "島嶼"},
}

labels["歐盟"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[歐盟]]相關的詞語。",
	parents = {"歐洲"},
}

labels["加斯科涅"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[加斯科涅]]相關的詞語。",
	parents = {"法國"},
}

labels["印度次大陸"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[印度次大陸]]相關的詞語。",
	parents = {"南亞"},
}

labels["孟加拉"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[孟加拉]]地區相關的詞語。",
	parents = {"印度次大陸"},
}

labels["克什米爾"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[克什米爾]]相關的詞語。",
	parents = {"印度次大陸"},
}

labels["克什米爾(印度)"] = {
	type = "related-to",
	description = "{{{langname}}}中[[印度]]管轄的[[克什米爾]]地名。",
	parents = {"印度", "克什米爾"},
}

labels["朝鮮半島"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[朝鮮半島]]相關的詞語。",
	parents = {"亞洲"},
}

labels["朗格多克"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[朗格多克]]相關的詞語。",
	parents = {"法國"},
}

labels["拉普蘭"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[拉普蘭]]相關的詞語。",
	parents = {"歐洲", "芬蘭", "挪威", "俄羅斯", "瑞典"},
}

labels["中東"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[中東]]相關的詞語。",
	parents = {"非洲", "亞洲"},
}

labels["荷屬安的列斯"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[荷屬安的列斯]]相關的詞語。",
	parents = {"荷蘭", "北美洲"},
}

labels["海外法國"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[海外法國]]相關的詞語。",
	parents = {"法國"},
}

labels["普羅旺斯"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[普羅旺斯]]相關的詞語。",
	parents = {"法國"},
}

labels["波蘭人民共和國"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[波蘭人民共和國]]相關的詞語。",
	parents = {"波蘭"},
}

labels["南亞"] = {
	type = "related-to",
	description = "{{{langname}}}中與[[南亞]]相關的詞語。",
	parents = {"歐亞大陸", "亞洲"},
}

return {LABELS = labels, HANDLERS = handlers}