Module:Geography
Appearance
Documentation for this module may be created at Module:Geography/doc
-- ================================================================================
-- Module dependencies
-- ================================================================================
local getArgs = require('Module:Arguments').getArgs -- for processing arguments
local ibf = require("Module:Infobox_functions")
local json = require("Module:JSON_pipeline")
-- ================================================================================
-- Main table
-- ================================================================================
local p = {}
-- ================================================================================
-- Resolve a County/State highlight-map filename on Commons
--
-- The naive "Map of {state} highlighting {county}.svg" guess (built straight
-- from the Census NAMELSAD-derived page title) is correct for the large
-- majority of counties, but Commons' actual file names diverge in a couple
-- of systematic, checkable ways -- confirmed against Category:Pages_with_
-- broken_file_links (2026-09-08): every failure fell into exactly these
-- buckets:
-- - Census's NAMELSAD lowercases the "city" in independent cities (e.g.
-- "Richmond city, Virginia"); Commons always capitalizes it ("...
-- highlighting Richmond City.svg"). Covers all VA independent cities
-- plus Baltimore city, MD.
-- - Commons spells "St." out as "Saint" (St. Croix/Francois/Joseph/Louis
-- counties, and combined with the city-capitalization rule for
-- St. Louis *city* -> "Saint Louis City.svg").
-- - A small number of genuine one-offs where Commons' name doesn't derive
-- from the Census name by any rule at all (e.g. Skagway Municipality,
-- Alaska -> "Skagway City and Borough.svg", matching the same kind of
-- Alaska borough-naming quirk as Yakutat from the geo QID work) --
-- these get a literal override, not a transform.
--
-- Checks candidates via mw.title.new(...).exists rather than guessing, so it
-- self-heals if Commons ever renames a file -- no periodic re-verification
-- step needed the way the R-side geo QID crosswalk needs one. Returns nil
-- (not an error, not a broken-link placeholder) if nothing matches, so the
-- caller can just skip rendering the image.
-- ================================================================================
local county_map_file_overrides = {
["Skagway Municipality, Alaska"] = "Map of Alaska highlighting Skagway City and Borough.svg",
}
local function resolve_county_map_file(title, state, county)
if county_map_file_overrides[title] then
return county_map_file_overrides[title]
end
if not state or not county then
return nil
end
local candidates = { "Map of " .. state .. " highlighting " .. county .. ".svg" }
local city_base = county:match("^(.+) city$")
if city_base then
table.insert(candidates, "Map of " .. state .. " highlighting " .. city_base .. " City.svg")
end
local saint_rest = county:match("^St%. (.+)$")
if saint_rest then
table.insert(candidates, "Map of " .. state .. " highlighting Saint " .. saint_rest .. ".svg")
end
-- Both transforms at once (e.g. "St. Louis city" -> "Saint Louis City")
if city_base then
local city_saint_rest = city_base:match("^St%. (.+)$")
if city_saint_rest then
table.insert(candidates, "Map of " .. state .. " highlighting Saint " .. city_saint_rest .. " City.svg")
end
end
for _, candidate in ipairs(candidates) do
local file_title = mw.title.new("File:" .. candidate)
-- file_title.exists only checks for a LOCAL page at that title --
-- always false for a Commons-hosted file with no local description
-- page, regardless of whether the file itself is real. file_title.file
-- is the Scribunto object that actually resolves through the file
-- repo (including foreign/Commons repos) -- same accessor
-- Module:Multiple image (standard Wikipedia core module) uses to
-- check file existence/dimensions.
local file_obj = file_title and file_title.file
if file_obj and file_obj.exists then
return candidate
end
end
return nil
end
-- ================================================================================
-- County infobox
-- ================================================================================
function p.county_infobox(frame)
local args = getArgs(frame)
local title = mw.title.getCurrentTitle().text
local focal_county = title
-- Extract naked state and county names from the title
local function split_on_comma(s)
local before = s:match("^(.-),") or s
local after = s:match(",%s*(.+)$") or nil
return before, after
end
local county, state = split_on_comma(title)
-- ========================================
-- Map image management for country and administrative unit
-- ========================================
local image = resolve_county_map_file(title, state, county)
-- ========================================
-- UI / Theming
-- ========================================
local html_theme_class = "-base" -- or "-species" if that's the standard for geography
local subtitle = state and ("Location within " .. state) or ""
-- ========================================
-- Build infobox HTML
-- ========================================
local root = ""
-- `county` (e.g. "Crawford County"), not the full `title` (e.g.
-- "Crawford County, Wisconsin") -- the state already shows in `subtitle`
-- ("Location within Wisconsin"), so using the full title here would just
-- repeat it.
root = ibf.create_infobox(root, county, subtitle, html_theme_class)
-- Add image if available
if image then
local image_options = {
link = state, -- clicking the image links to the state
caption = ""
}
root = ibf.add_image(root, image, html_theme_class, image_options)
end
return root
end
-- ================================================================================
-- State infobox
-- ================================================================================
function p.state_infobox(frame)
local args = getArgs(frame)
local title = mw.title.getCurrentTitle().text
local subtitle = "Location within the USA"
-- ========================================
-- Cargo query
-- ========================================
-- cargo query to find state metadata for focal geo
local tables = ""
local fields = ""
local cargo_args = {}
-- ========================================
-- Map image management for country and administrative unit
-- ========================================
local image = title .. " in United States.svg" -- generic MediaWiki Commons formatting for State map images
-- ========================================
-- Create html output for infobox
-- ========================================
-- Begin creating the html table infobox template
local html_theme_class = "-species"
local root = ""
root = ibf.create_infobox(root, title, subtitle, html_theme_class)
root = ibf.add_image(root, image, html_theme_class)
return root
end
-- get_county_list_for_state() lives further down, after resolve_geo_qid()'s
-- definition -- it depends on that local function, which must come first.
-- ================================================================================
-- Resolve a geography page title to its QID
--
-- Cold-start lookup against JSON:Title_to_geo_QID_map (see
-- ww_03d_title_to_geo_qid_map.R), used by get_species_list_for_geography()
-- below. Returns (qid, err) -- err is nil on success.
-- ================================================================================
local function resolve_geo_qid(title)
-- load_dataset_multi() (not the simpler json.lookup()) because this
-- dataset can be chunked via a "_index" routing page once it grows past
-- its char_limit (build_json_pages_from_structure()'s convention) --
-- json.lookup() only recognizes a "_meta"-suffixed routing page, a
-- different chunking convention from a different R-side builder, and
-- silently returns nil against this one instead of erroring. See the
-- identical fix on JSON:Geography_to_species below -- that dataset is
-- already chunked in production (18 pages), which is exactly how this
-- was caught: it silently broke there first.
local records, fields = json.load_dataset_multi("JSON:Title_to_geo_QID_map")
local raw = records and json.get(records, title)
local record = raw and json.zip(fields, raw)
local qid = record and record.QID
if not qid or qid == "" then
return nil, "No geography QID found for '" .. title .. "'."
end
return qid, nil
end
-- ================================================================================
-- Species list for a State/County page, by QID
--
-- Replaces the old baked-in-wikitext species list on Data:State-*/
-- Data:County-* (see ww_03f_geo_species_json.R's header for why): resolves
-- the current page's QID, looks up its record in JSON:Geography_to_species,
-- and renders the same "===[[Rodentia]]===" / "===[[Eulipotyphla]]==="
-- bulleted-list wikitext shape the baked version used to produce, so the
-- rendered page looks the same -- only the mechanism changed.
-- ================================================================================
function p.get_species_list_for_geography(frame)
local title = mw.title.getCurrentTitle().text
local qid, qid_err = resolve_geo_qid(title)
if qid_err then return "" end -- no QID yet (e.g. still-pending ledger entry) -- render nothing rather than an error on a live data page
-- load_dataset_multi(), not json.lookup() -- see resolve_geo_qid()'s
-- comment above. This dataset is confirmed chunked live (18 pages via
-- JSON:Geography_to_species_index), which is exactly the case
-- json.lookup() doesn't handle.
local records, fields = json.load_dataset_multi("JSON:Geography_to_species")
local raw = records and json.get(records, qid)
local record = raw and json.zip(fields, raw)
if not record then return "" end
local function render_section(heading, species_list)
if type(species_list) ~= "table" or #species_list == 0 then
return ""
end
local sorted = {}
for _, v in ipairs(species_list) do table.insert(sorted, v) end
table.sort(sorted)
local lines = { "===[[" .. heading .. "]]===" }
for _, species_link in ipairs(sorted) do
table.insert(lines, "* " .. species_link)
end
return table.concat(lines, "\n")
end
local sections = {}
local rodentia_section = render_section("Rodentia", record.Rodentia)
local eulipotyphla_section = render_section("Eulipotyphla", record.Eulipotyphla)
if rodentia_section ~= "" then table.insert(sections, rodentia_section) end
if eulipotyphla_section ~= "" then table.insert(sections, eulipotyphla_section) end
return table.concat(sections, "\n")
end
-- ================================================================================
-- Get county lists for state, by QID
--
-- Replaces the old Cargo-table query (Template:State's #cargo_store /
-- get_county_list_for_state() querying the "State" table by State_name --
-- a name-based lookup, exactly the fragile pattern the QID rosetta stone
-- project exists to eliminate). The State Cargo table's only real consumer
-- was this function, and it's not crowd-sourced content -- it's geography
-- scaffolding the wiki's structure depends on -- so there's no reason to
-- keep a whole Cargo table/Data:State-* page around just to serve it.
-- Resolves the current page's QID and looks up County/County_state in the
-- same JSON:Geography_to_species dataset the species lists above use (see
-- ww_03f_geo_species_json.R) -- those two fields are populated for state
-- QIDs only, empty lists for county QIDs.
-- ================================================================================
function p.get_county_list_for_state(frame)
local title = mw.title.getCurrentTitle().text
local qid, qid_err = resolve_geo_qid(title)
if qid_err then
return "<noinclude><!-- No counties found for state '" .. title .. "': " .. qid_err .. " --></noinclude>"
end
local records, fields = json.load_dataset_multi("JSON:Geography_to_species")
local raw = records and json.get(records, qid)
local record = raw and json.zip(fields, raw)
if not record or type(record.County) ~= "table" or #record.County == 0 then
return "<noinclude><!-- County list empty for state '" .. title .. "' --></noinclude>"
end
local counties = record.County
local county_states = record.County_state or {}
local list_items = {}
for i, display_raw in ipairs(counties) do
local display = mw.text.trim(display_raw or "")
-- Strip ", Arizona" (or any ", <something>") from the display text.
-- Example: "Maricopa County, Arizona" -> "Maricopa County"
display = display:gsub("%s*,.*$", "")
local target = mw.text.trim(county_states[i] or "")
if display ~= "" then
if target ~= "" then
table.insert(list_items, string.format("* [[%s|%s]]", target, display))
else
-- Fallback if no page stored
table.insert(list_items, string.format("* %s", display))
end
end
end
-- Leading/trailing newlines help when nested inside templates
return "\n" .. table.concat(list_items, "\n") .. "\n"
end
return p