Module:Wikidata Infobox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

The Lua code behind {{Wikidata Infobox}}

DO NOT EDIT THIS MODULE DIRECTLY!

Please make any changes at Module:Wikidata Infobox/sandbox to be tested first.

Changes are coordinated at Template talk:Wikidata Infobox and deployed in batches to reduce server load, as modifying this page triggers an update of over 5 million Commons categories.

Code

local p = {}
require('strict')
local WikidataIB = require( 'Module:WikidataIB' )
local i18n = require( 'Module:Wikidata Infobox/i18n' ).i18n
local getBestStatements = mw.wikibase.getBestStatements
local frame = mw.getCurrentFrame()

local config = {
	-- toggle/customize infobox features:mw
	defaultsort = true,
	interwiki = true,
	autocat = true,
	trackingcats = true,
	uploadlink = true,
	sitelinks = true,
	authoritycontrol = true,
	helperlinks = true,
	coordtemplate = 1, -- 0 = none, 1 = Geohack, 2 = Coord
	mapwidth = 250,
	mapheight = 250,
	imagesize = '230x500px',

	-- parameters for WikidataIB:
	spf = '',        -- suppressfields
	fwd = 'ALL',     -- fetchwikidata
	osd = 'no',      -- onlysourced
	noicon = 'yes',  -- pencil icon
	wdlinks = 'id',  -- add links to Wikidata if no label found
	collapse = 10,   -- collapse list of values if too many values
	maxvals = 30,    -- stop fetching Wikidata after this number of values
}

-- variables set by main():
local ITEM            -- mw.wikibase.entity table
local QID             -- qid of ITEM, e.g. 'Q42'
local CLAIMS          -- ITEM.claims
local ISTAXON         -- whether ITEM is a biological taxon
local INSTANCEOF = {} -- Hash set of ITEM's best "instance of" values
local MYLANG          -- user's languge code
local LANG            -- language object of user's language
local FALLBACKLANGS   -- list containing MYLANG and its fallback languages

-- Can't have more than one {{#coordinates:primary}}, so keep track of count
local primary_coordinates = 0

--- Returns label of given Wikidata entity in user's language.
--- If label doesn't exist, returns the id as link to Wikidata.
--- @param id string
--- @param nolink? boolean: Whether to return link to Wikidata if no label found
local function getLabel( id, nolink )
	local label = mw.wikibase.getLabel( id )
	if label then
		return mw.text.nowiki( label ) -- nowiki to prevent wikitext injection
	elseif nolink then
		return id
	else
		return '[[d:' .. id .. ']]'
	end
end

--- Query Wikidata entity for the first best value of property _pid_.
--- Returns nil if first best value is novalue or somevalue.
--- Returns nil if entityOrId is neither table nor string.
--- @param entityOrId table|string: getEntity() or qid.
--- @param pid string
--- @return unknown|nil
local function getSingleValue( entityOrId, pid )
	local claim
	if type( entityOrId ) == 'table' then
		claim = entityOrId:getBestStatements( pid )[1]
	elseif type( entityOrId ) == 'string' then
		claim = getBestStatements( entityOrId, pid )[1]
	end
	return claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
end

--- Iterator function over a list of Wikidata claims/statements
--- @param t table as returned by wikibase.getBestStatements
local function iclaims( t )
	local i = 1
	return function()
		while i <= #t do
			local dv = t[i].mainsnak.datavalue
			local v = dv and dv.value
			i = i + 1
			if v then return v end
		end
	end
end

--- Returns Commons sitelink (full page title), preferably to category
--- @param qid string
--- @return string|nil
local function getCommonsLink( qid )
	local sitelink = mw.wikibase.getSitelink( qid, 'commonswiki' )
	if sitelink and sitelink:sub(1,9) == 'Category:' then
		return sitelink  -- sitelink to category page
	end

	local maincat = getSingleValue( qid, 'P910' )  -- topic's main category
	if maincat and maincat.id then
		local sl = mw.wikibase.getSitelink( maincat.id, 'commonswiki' )
		if sl then return sl end
	end

	local listcat = getSingleValue( qid, 'P1754' )  -- category related to list
	if listcat and listcat.id then
		local sl = mw.wikibase.getSitelink( listcat.id, 'commonswiki' )
		if sl then return sl end
	end

	local P373 = getSingleValue( qid, 'P373' )  -- Commons category
	if P373 then
		return 'Category:' .. P373
	end

	return sitelink  -- sitelink to gallery page
end

local getSitelink = (mw.wikibase.getGlobalSiteId() == 'commonswiki') and getCommonsLink or mw.wikibase.getSitelink

--- Returns sitelink to Commons as wikilink or the label of the given Q-item
--- @param qid string
local function getLinkOrLabel( qid )
	local sitelink = getSitelink( qid )
	if sitelink then
		return "[[:" .. sitelink .. "|" .. getLabel( qid, true ) .. "]]"
	else
		return getLabel( qid )
	end
end

--- Renders snak as rich wikitext. Returns nil if snak is nil or false.
--- @param snak table: claim.mainsnak or claim.qualifiers[pid]
local function renderSnak( snak )
	if not snak then return end
	local snaktype = snak.snaktype
	if snaktype == 'value' then
		local datatype = snak.datatype
		local value = snak.datavalue.value
		if datatype == 'wikibase-item' then
			return getLinkOrLabel( value.id )
		else
			return mw.wikibase.formatValue( snak )
		end
	elseif snaktype == 'somevalue' then
		local label = mw.message.new('Wikibase-snakview-variations-somevalue-label'):inLanguage(MYLANG):plain()
		return '<i style="color:#54595d">'..label..'</i>'
	end
end

--- Returns claim whose "language of work or name" (P407) qualifier matches
--- langcode, or nil if none matches.
--- @param claims table as returned by getBestStatements()
--- @param langcode string, e.g. "en"
--- @return unknown|nil
local function getClaimByLang( claims, langcode )
	for _, claim in ipairs( claims or {} ) do
		for _, qual in ipairs( claim.qualifiers and claim.qualifiers['P407'] or {} ) do
			if qual.datavalue and qual.datavalue.value and getSingleValue( qual.datavalue.value.id, 'P424' ) == langcode then
				return claim
			end
		end
	end
end

--- If the given snaks of datatype monolingualtext contain a string in one of
--- the user's fallback languages, the string is returned; otherwise a random
--- string is retuned. The second return value indicates whether finding a
--- string in one of the user's fallback languages was successful.
--- @param snaks table, e.g. claims.qualifiers['P2096']
--- @return string?, boolean? success
local function extractMonolingualText( snaks )
	if not snaks or snaks == {} then return end

	-- collect strings into hash table with langcodes as keys
	local monotext = {}
	for _, snak in ipairs( snaks ) do
		local ms = snak.mainsnak or snak
		local v = ms and ms.datavalue and ms.datavalue.value
		if v then
			monotext[v.language] = v.text
		end
	end

	for _, lang in ipairs( FALLBACKLANGS ) do
		if monotext[lang] then return monotext[lang], true end
	end

	-- return random string
	local _, v = next( monotext )
	return v, false
end

--- Parses a string in WikiHiero syntax
local function expandhiero( hiero )
	return frame:callParserFunction{ name = '#tag:hiero', args = {hiero} }
end

--- Returns a string containing two table rows
local function format2rowline( header, content )
	return '<tr><th class="wikidatainfobox-lcell" style="text-align:left" colspan="2">'..header..'</th></tr><tr><td style="vertical-align:top" colspan="2">'..content..'</td></tr>'
end

--- Returns a string containing a single table row
local function format1rowline( trqid, header, content )
	return '<tr id="'..trqid..'"><th class="wikidatainfobox-lcell">'..header..'</th><td style="vertical-align:top">'..content..'</td></tr>'
end

--- Returns a string containing the HTML markup for an infobox row.
--- Returns nil if content is empty.
--- @param eid string: ID of Wikidata entity whose label shall be used as heading
--- @param content string|nil
--- @param mobile? boolean: Set to true to show on devices with narrow screens
local function formatLine( eid, content, mobile )
	if not content or content == '' then return end
	local row = mw.html.create( 'tr' )
	if not mobile then
		row:addClass( 'wdinfo_nomobile' ) -- [[Template:Wikidata_Infobox/styles.css]]
	end
	row:tag( 'th' )
		:addClass( 'wikidatainfobox-lcell' )
		:node( LANG:ucfirst( getLabel(eid) ) )
	row:tag( 'td' )
		:node( content )
	return tostring( row )
end

--- Returns unbulleted HTML list if given a sequence table.
--- @param list string[]
local function ubl( list )
	if #list == 0 then return end
	local out = table.concat( list, '</li><li>' )
	return '<div class="plainlist"><ul><li>'..out..'</li></ul></div>'
end

--- Given a language code, returns its databaseId (as used by Wikidata sitelinks).
--- All databaseIds that a wiki knows are stored in its [[mw:Manual:sites table]].
--- @param langcode string
local function databaseId( langcode )
	local exceptions = {
		['be-tarask'] = 'be_x_old',     -- Belarusian (Taraškievica orthography)
		['bho']       = 'bh',           -- Bhojpuri
		['cbk-zam']   = 'cbk_zam',      -- Chavacano de Zamboanga
		['gsw']       = 'als',          -- Alemannic
		['ike']       = 'iu',           -- Inuktitut
		['lzh']       = 'zh_classical', -- Classical Chinese
		['map-bms']   = 'map_bms',      -- Basa Banyumasan
		['nan']       = 'zh_min_nan',   -- Min Nan Chinese
		['nb']        = 'no',           -- Norwegian Bokmål
		['nds-nl']    = 'nds_nl',       -- Low Saxon
		['mo']        = 'ro',           -- Moldaawisk
		['roa-tara']  = 'roa_tara',     -- Tarantino
		['rup']       = 'roa_rup',      -- Aromanian
		['sgs']       = 'bat_smg',      -- Samogitian
		['vro']       = 'fiu_vro',      -- Võro
		['yue']       = 'zh_yue',       -- Cantonese
		-- I did my best to make this list as comprehensive as possible.
		-- Useful pages for finding exceptions:
		-- [[mw:Manual:$wgExtraLanguageCodes]]
		-- [[meta:Special_language codes]]
		-- [[meta:List_of_Wikipedias#Nonstandard_language_codes]]
		-- [[meta:Template:N en/list]]
		-- [[meta:Template:Wikilangcode]]
	}

	local exception = exceptions[langcode]
	if exception then return exception end

	return langcode:gsub("-.*", "") -- delete everything after hyphen
end

-- Set of pids whose values should always be linked even if they are collapsed.
-- Adding new pids may slow down the infobox on certain pages.
local should_be_linked = {
	-- pid          property label             rationale
	P2789=true,  -- connects with              [[Template_talk:Wikidata_Infobox/Archive_5#P2789_-_connects_with]]
	P527=true,   -- has part(s)                [[Template_talk:Wikidata_Infobox/Archive_5#P2789_-_connects_with]]
	P1382=true,  -- partially coincident with  [[Template_talk:Wikidata_Infobox/Archive_5#P1382_vs._P527]]
	P40=true,    -- child                      [[Template_talk:Wikidata_Infobox#No_links_if_information_is_folded?]]
	P3373=true,  -- sibling                    [[Template_talk:Wikidata_Infobox#No_links_if_information_is_folded?]]
}

--- Wrapper around WikidataIB. Returns nil if the item has no _pid_ statement.
--- @param pid string: Wikidata property id
--- @param args? table: arguments for WikidataIB
--- @return string|nil
local function getValue( pid, args )
	args = args or {}

	local collapse = args.collapse or config.collapse
	if collapse == 0 and args.linked == nil then
		error("getValue: Must give linked='no' or linked='yes' if collapse=0", 2)
	end

	-- linking many values harms performance if the value items are big and the sitelink needs to be taken from P910, P1754 or P373
	local linked = args.linked or should_be_linked[pid]
			or #getBestStatements(args.qid or QID, pid) <= collapse

	return WikidataIB._getValue{
		pid,
		name = pid,
		qid = args.qid or QID,
		linked = linked,
		wdlinks = args.wdlinks or config.wdlinks,
		prefix = args.prefix,
		postfix = args.postfix,
		linkprefix  = ':', -- suppress categorization
		qlinkprefix = ':', -- suppress categorization
		sorted = args.sorted,
		qual = args.qual or 'MOST',
		qualsonly = args.qualsonly,
		maxvals = args.maxvals or config.maxvals,
		postmaxvals = '…',
		collapse = collapse,
		spf = args.spf or config.spf,
		fwd = args.fwd or config.fwd,
		osd = args.osd or config.osd,
		rank = 'best',
		noicon = args.noicon or config.noicon,
		list = args.list or 'Unbulleted list',
		sep = args.sep,
		unitabbr = args.unitabbr,
		df = args.df, -- date format
		plaindate = args.plaindate,
		lang = args.lang,
		gendered = args.gendered,
	}
end

--- Used if no custom logic was specified for pid.
local function defaultFunc( pid, args )
	return formatLine( pid, getValue(pid, args) )
end

local function defaultFuncMobile( pid, args )
	return formatLine( pid, getValue(pid, args), true )
end
local function defaultFuncMobileGendered( pid )
	return formatLine( pid, getValue(pid, {gendered=true}), true )
end

local function getAudio( pid )
	local audiofile = getSingleValue( ITEM, pid )
	return audiofile and formatLine( pid, '[[File:' .. audiofile .. '|100px]]' )
end

local function getAudioByLang( pid )
	local claims = ITEM:getBestStatements( pid )
	local claim = claims[1]
	for i = 1, #FALLBACKLANGS do
		local c = getClaimByLang( claims, FALLBACKLANGS[i] )
		if c then
			claim = c
			break
		end
	end
	local audiofile = claim and claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
	return audiofile and formatLine( pid, '[[File:' .. audiofile .. '|100px]]' )
end

-- Example at [[Category:Thutmosis III]]
local function getHieroglyphs()
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements('P7383') ) do -- name in hiero markup
		local idv = v.mainsnak.datavalue.value
		if v.qualifiers and v.qualifiers['P3831'] then
			for _, w in ipairs( v.qualifiers['P3831'] ) do
				if w.datavalue then
					local label = getLabel( w.datavalue.value.id )
					rows[#rows+1] = format2rowline( label, expandhiero(idv) )
				end
			end
		else
			rows[#rows+1] = format2rowline( getLabel('Q82799', true), expandhiero(idv) )
		end
	end
	return table.concat( rows )
end

--- WikidataIB arguments for birth and death related properties
local birthdeath_args = { list = '', quals = table.concat({
	'P4241',  -- refine date
	'P805',   -- statement is subject of
	'P1932',  -- object stated as
	'P1810',  -- subject named as
	'P5102',  -- nature of statement
	'P1480',  -- sourcing circumstances
	'P459',   -- determination method
	'P1013',  -- criterion used
	'P1441',  -- present in work
	'P10663', -- applies to work
}, ',') }

local function getBirth( pid )
	local out = {}
	out[#out+1] = getValue( pid, birthdeath_args )                     -- date
	out[#out+1] = CLAIMS['P19'] and getValue( 'P19', birthdeath_args ) -- place
	out[#out+1] = extractMonolingualText( ITEM:getBestStatements('P1477') ) -- name
	return formatLine( pid, table.concat(out, '<br>') )
end

local function getDeath( pid )
	local out = {}
	out[#out+1] = getValue( pid, birthdeath_args )                     -- date
	out[#out+1] = CLAIMS['P20'] and getValue( 'P20', birthdeath_args ) -- place
	return formatLine( pid, table.concat(out, '<br>') )
end

local function getWebsite( pid )
	for _, claim in ipairs( ITEM:getBestStatements(pid) ) do
		local quals = claim.qualifiers
		local url = claim.mainsnak.datavalue and claim.mainsnak.datavalue.value
		if url and not (quals and quals['P582']) then -- no "end time" qualifier
			return '<tr><td colspan=2 style="text-align:center">['..url..' '..getLabel(pid)..']</td></tr>'
		end
	end
end

local function getSignature( pid )
	local img = getSingleValue( ITEM, pid )
	if img then
		local alt = LANG:ucfirst( getLabel(pid, true) )
		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center"><span class="wpImageAnnotatorControl wpImageAnnotatorCaptionOff">[[File:'..img..'|150px|alt='..alt..']]</span></td></tr>'
		-- equivalent to {{ImageNoteControl | caption=off | type=inline}}
	end
end

--- If ITEM has a pid statement, this behaves exactly like defaultFunc. Otherwise
--- figures out the region that ITEM is in and queries the region item for pid.
--- It finds the region by first checking if ITEM has a regionPid value.
--- Otherwise it takes the region from the P971 (category combines topics)
--- statement of ITEM's main category.
--- Examples at Cat:Health_in_Gabon, Cat:Economy_of_Germany, Q7246071
--- @param pid string, e.g. "P2250" for life expectancy
--- @param regionPid string: usually "P17" (country) or "P276" (location)
--- @param collapse number: argument for WikidataIB
local function getByRegion( pid, regionPid, collapse )
	local region = getSingleValue( ITEM, regionPid )
	if CLAIMS[pid] then
		region = QID
	elseif region then
		region = region.id
	else
		local maincat = getSingleValue( ITEM, 'P910' ) -- topic's main category
		if maincat then
			for topic in iclaims( getBestStatements(maincat.id, 'P971') ) do
				local id = topic.id
				if id ~= 'Q12147' and id ~= 'Q8434' and id ~= 'Q159810' then
					-- assume id is QID of a region if it's not the QID for "health", "education", or "economy"
					region = id
				end
			end
		end
	end
	return region and defaultFunc( pid, {
		qid = region,
		collapse = collapse,
	})
end
local function getByCountry( pid )
	return getByRegion( pid, 'P17', 10 )
end
local function getByLocation( pid )
	return getByRegion( pid, 'P276', 10 )
end
local function getByLocationCollapse4( pid )
	return getByRegion( pid, 'P276', 4 )
end

local function getPrimeFactors()
	local out = {}
	for _, claim in ipairs( ITEM:getBestStatements('P5236') ) do
		local quals = claim.qualifiers and claim.qualifiers['P1114']
		local quantity = quals and quals[1].datavalue.value.amount
		if quantity then
			quantity = quantity:sub(2)  -- strip plus sign
			out[#out+1] = renderSnak(claim.mainsnak) .. '<sup>'..quantity..'</sup>'
		else
			out[#out+1] = renderSnak(claim.mainsnak)
		end
	end
	return formatLine( 'Q4846249', table.concat(out, ' × ') )
end

local function getUnicodeChars( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		if v.mainsnak.datavalue then
			local idv = v.mainsnak.datavalue.value
			for _, w in ipairs( v.qualifiers and v.qualifiers['P3831'] or {} ) do
				if w.datavalue then
					local qualid = w.datavalue.value.id
					rows[#rows+1] = format1rowline( qualid, getLabel(qualid), idv )
				end
			end
		end
	end
	return table.concat( rows )
end

local function getCodes( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		if v.mainsnak.datavalue then
			local idv = v.mainsnak.datavalue.value
			for _, w in ipairs( v.qualifiers and v.qualifiers['P3294'] or {} ) do
				if w.datavalue then
					local qualid = w.datavalue.value.id
					if qualid == "Q68101340" then
						idv = expandhiero( idv )
					end
					rows[#rows+1] = format1rowline( qualid, getLinkOrLabel(qualid), idv )
				end
			end
		end
	end
	return table.concat( rows )
end

local function getCodeImages( pid )
	local rows = {}
	for _, v in ipairs( ITEM:getBestStatements(pid) ) do
		local idv = v.mainsnak.datavalue.value
		for _, w in ipairs( v.qualifiers and v.qualifiers['P3294'] or {} ) do
			if w.datavalue then
				local qualid = w.datavalue.value.id
				local img = '[[File:' .. idv .. '|none|35px]]'
				rows[#rows+1] = format1rowline( qualid, getLabel(qualid), img )
			end
		end
	end
	return table.concat( rows )
end

local function getLocation()
	local function fallback()
		local set = {} -- locations as keys
		local out = {} -- locations as values
		for _, pid in ipairs{ 'P706', 'P276', 'P131', 'P17' } do
			for _, claim in ipairs( ITEM:getBestStatements(pid) ) do
				local location
				if pid == 'P17' then  -- don't link to countries
					local dv = claim.mainsnak.datavalue
					location = dv and getLabel( dv.value.id )
				else
					location = renderSnak( claim.mainsnak )
				end
				if location and not set[location] then
					local n = #out + 1
					set[location] = true     -- we don't want duplicate values
					out[n]        = location -- we want to preserve the order

					if n > config.maxvals then
						out[n] = '…' -- postmaxvals
						return formatLine( 'P276', ubl(out) )
					end
				end
			end
		end
		return formatLine( 'P276', ubl(out) )
	end

	local P131,P276,P706 = CLAIMS['P131'] or {}, CLAIMS['P276'] or {}, CLAIMS['P706'] or {}
	if (#P131 < 2) and (#P276 < 2) and (#P706 < 2) then
		return formatLine( 'P276', WikidataIB.location{ args={QID} } ) or fallback()
	else
		return fallback()
	end
end

local function getAuthors()
	if CLAIMS['P50'] or CLAIMS['P2093'] then
		local args = { list='', sep='</li><li>', collapse=0, maxvals=10, linked='yes', qual='P1545,P518,P5102,P3831' }
		local authors = getValue( 'P50', args ) or ''
		local namestrings = getValue( 'P2093', args )
		return formatLine( 'P50', ubl{authors, namestrings} )
	end
end

local function getDifferentFrom()
	local out = {}
	local i = 0
	for different in iclaims( ITEM:getBestStatements('P1889') ) do
		i = i + 1
		if i > config.maxvals then break end
		local href = getSitelink( different.id ) or ( 'd:'..different.id )
		local label = getLabel( different.id, true )

		local class = getSingleValue( different.id, 'P31' )
		local isdab = class and class.id == 'Q4167410'
		local icon = isdab and ' [[File:Disambig.svg|18px|alt='..mw.wikibase.getLabel('Q4167410')..']]'

		local desc = mw.wikibase.getDescription( different.id )
		if desc then
			label = '<span title="'..mw.text.nowiki(desc)..'">'..label..'</span>'
		end

		out[#out+1] = string.format( '[[:%s|%s]]%s', href, label, icon or '' )

	end
	return formatLine( 'P1889', ubl(out) )
end

--- Returns common taxon name using [[Module:Wikidata4Bio]]
local function getVernacularName()
	if ISTAXON then
		local vn = frame:expandTemplate{ title = 'VNNoDisplay', args = {
			useWikidata = QID
		}}
		if vn:sub(3,10) ~= 'Category' and not vn:match('class="error') then
			-- we found at least one common name and there are no errors
			local label = LANG:ucfirst( getLabel('Q502895') )
			return '<tr><td colspan=2><table style="width:100%"><tr><th style="background: #cfe3ff>'..label..'</th></tr><tr><td><div style="overflow-wrap: break-word" class="mw-collapsible mw-collapsed wikidatainfoboxVN" id="wdinfoboxVN">'..vn..'</div></td></tr></table></td></tr>'
		end
	end
end

local function getTaxontree()
	local content = require('Module:Taxontree').show{ args = {
		qid = QID,
		authorcite = 'y',
		first = 'y',
	}}
	local label = LANG:ucfirst( getLabel('Q8269924') )
	return '<tr><td colspan=2><table style="width:100%" id="wdinfo_taxon" class="mw-collapsible"><tr><th style="background: #cfe3ff" colspan=2>'..label..'</th></tr>'..content..'</table></td></tr>'
end

local function getOriginalCombination()
	local ocomb = getSingleValue( ITEM, 'P1403' )
	ocomb = ocomb and ocomb.id
	local taxoname = ocomb and getSingleValue( ocomb, 'P225'  ) or ''
	local citation = ocomb and getSingleValue( ocomb, 'P6507' ) or ''
	if taxoname then
		return formatLine( 'P1403', '<i>'..taxoname..'</i>' .. ' ' .. citation )
	end
end

--- Creates a taxon author citation from P405 and P574 qualifiers if
--- P6507 (taxon author citation as string) not present since otherwise
--- Taxontree already shows the citation.
local function getTaxonAuthor()
	local claims = CLAIMS['P225'] -- P225 = taxon name
	if #claims > 1 then
		return defaultFunc( 'P225' ) -- Example at [[Category:Acacia stricta]]
	elseif #claims == 1 then
		if CLAIMS['P6507'] then -- P6507 = taxon author citation (string)
			return -- Taxontree already shows citation, see [[Ophiogymna]]
		end
		local quals = claims[1].qualifiers
		local author = renderSnak( quals and quals['P405'] and quals['P405'][1] )
		local year = renderSnak( quals and quals['P574'] and quals['P574'][1] )
		if author and year then
			return formatLine( 'P405', author .. ', ' .. year )
		elseif year then
			return formatLine( 'P574', year ) -- [[Cat:Porphyrophora polonica]]
		end
	end
end

--- Given an area, returns a map zoom level to use with mw:Extension:Kartographer.
--- Fallback output is 15.
local function autoMapZoom( area )
	if not area then return 15 end
	if area.unit == 'http://www.wikidata.org/entity/Q35852' then  -- hectare
		area = area.amount / 100  -- convert to km²
	elseif area.unit == 'http://www.wikidata.org/entity/Q25343' then  -- m²
		area = area.amount / 1e6  -- convert to km²
	elseif area.unit == 'http://www.wikidata.org/entity/Q81292' then  -- acre
		area = area.amount * 0.004  -- convert to km²
	else
		area = tonumber( area.amount )  -- assume the unit is km²
	end
	local LUT = { 5000000, 1000000, 100000, 50000, 10000, 2000, 150, 50, 19, 14, 5, 1, 0.5 }
	for zoom, scale in ipairs( LUT ) do
		if area > scale then
			return zoom + 1
		end
	end
	return 15
end

local function getCoordinates( pid )
	local coords = getSingleValue( ITEM, pid )
	if coords then
		local out
		local long = coords.longitude
		local lat  = coords.latitude
		local globeId = coords.globe:match( "Q%d+" )
		if globeId == 'Q2' then -- coords are on Earth
			local externaldata = { -- [[mw:Help:Extension:Kartographer]]
				type = "ExternalData",
				service = "geoshape",
				ids = QID,
				properties = {
					['fill'] = "#999999",
					['stroke'] = "#636363",
					['stroke-width'] = 2
				}
			}

			-- detect roads, mountain passes, rivers, borders etc.
			if CLAIMS['P2043'] or CLAIMS['P16']        -- length, transport network
			or CLAIMS['P974']  or CLAIMS['P4552']      -- tributary, mountain range
			or CLAIMS['P177']  or CLAIMS['P1064']      -- crosses, track gauge
			or CLAIMS['P15']   or CLAIMS['P14']        -- route map, traffic sign
			or CLAIMS['P930']  or CLAIMS['P3858'] then -- electrification, route diagram
				externaldata.service = 'geoline'
				externaldata.properties['stroke'] = "#ff0000"
			end

			local geojson = {
				externaldata,
				{ type = "Feature",
				  geometry = { type="Point", coordinates = {long, lat} },
				  properties = {
				  	['marker-size'] = "medium",
				  	['marker-color'] = "006699"
				  },
				},
			}

			local zoom
			if CLAIMS['P402'] then  -- OpenStreetMap relation ID
				-- Let Kartographer figure out zoom level based on OSM geoshape.
				-- Kartographer uses [[mw:Wikimedia_Maps/API#OSM_Geoshapes_and_lines]]
				-- instead of P402 to find the OSM relation but there is no Lua
				-- interface for that. You can help adding P402 statements using
				-- https://mix-n-match.toolforge.org/#/catalog/688
			else
				local area = getSingleValue( ITEM, 'P2046' )
				zoom = autoMapZoom( area )
			end

			out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {
				frameless = 1,
				lang = MYLANG,
				width = config.mapwidth,
				height = config.mapheight,
				zoom = zoom,
				align = 'center',
			})
			if config.trackingcats then
				out = out ..'[[Category:Uses of Wikidata Infobox with maps]]'
			end
			if config.coordtemplate == 1 then
				if primary_coordinates == 0 then
					out = out .. frame:callParserFunction('#coordinates:primary', lat, long)
					primary_coordinates = 1
				end
				out = out .. '<small>'..require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, lang=MYLANG }..'</small>'
			elseif config.coordtemplate == 2 then
				local args = {
					display = 'inline,title',
					format = 'dms',
					nosave = 1,
					qid = QID
				}
				out = out .. '<small>'..frame:expandTemplate{ title = 'Coord', args = args }..'</small>'
			end
		else -- coords not on Earth
			local globe = mw.wikibase.getLabelByLang( globeId, 'en' ) or mw.wikibase.getLabelByLang( globeId, 'mul' )
			out = require('Module:Coordinates')._GeoHack_link{ lat=lat, lon=long, globe=globe, lang=MYLANG }
		end

		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..'</td></tr>'
	elseif config.trackingcats and (CLAIMS['P706'] or CLAIMS['P131']) then
		return '[[Category:Uses of Wikidata Infobox with no coordinate]]'
	end
end

--- Show map using [[mw:Help:Map Data]] if ITEM has no coordinates
local function getCommonsMapData()
	if CLAIMS['P625'] then return end
	local commonsdata = getSingleValue( QID, 'P3896' )
	if not commonsdata then return end
	local geojson = {{
		type = "ExternalData",
		service = 'page',
		title = commonsdata:sub(6),  -- strip "Data:" prefix
	}}
	local out = frame:extensionTag( 'mapframe', mw.text.jsonEncode(geojson), {
		frameless = 1,
		lang = MYLANG,
		width = config.mapwidth,
		height = config.mapheight,
		align = 'center',
	})
	if config.trackingcats then
		out = out ..'[[Category:Uses of Wikidata Infobox with maps]]'
	end
	return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">'..out..'</td></tr>'
end

local function getCelestialCoordinates()
	local ra = getSingleValue( ITEM, 'P6257' )  -- right ascension
	local de = getSingleValue( ITEM, 'P6258' )  -- declination
	if ra and de then
		local url = 'http://www.wikisky.org/?ra='..(ra.amount / 15)..'&de='..de.amount..'&de=&show_grid=1&show_constellation_lines=1&show_constellation_boundaries=1&show_const_names=1&show_galaxies=1&img_source=DSS2&zoom=9 '
		local ra_unit = getLabel( ra.unit:match('Q%d+') )
		local de_unit = getLabel( de.unit:match('Q%d+') )
		local ra_fmt = LANG:formatNum( tonumber(ra.amount) )
		local de_fmt = LANG:formatNum( tonumber(de.amount) )
		local text = LANG:ucfirst( getLabel('P6257') )..' '..ra_fmt..' '..ra_unit..
		     '<br>'..LANG:ucfirst( getLabel('P6258') )..' '..de_fmt..' '..de_unit
		return '<tr class="wdinfo_nomobile"><td colspan=2 style="text-align:center">['..url..text..']</td></tr>'
	end
end

local autocats_by_id = {
	P3596 = 'Archaeological monuments in Denmark with known IDs',
	P1371 = 'ASI monuments with known IDs',
	P2917 = 'Buildings of Madrid with COAM Register number',
	P3170 = 'Cultural heritage monuments in Armenia with known IDs',
	P2951 = 'Cultural heritage monuments in Austria with known IDs',
	P4244 = 'Cultural heritage monuments in Bavaria with known IDs',
	P2424 = 'Cultural heritage monuments in Berlin with known ID',
	P2948 = 'Cultural heritage monuments in Estonia (with known IDs)',
	P4009 = 'Cultural heritage monuments in Finland with known IDs',
	P380  = 'Cultural heritage monuments in France with known IDs',
	P4166 = 'Cultural heritage monuments in Georgia with known IDs',
	P1769 = 'Cultural heritage monuments in Hesse with known ID',
	P1369 = 'Cultural heritage monuments in Iran with known IDs',
	P1799 = 'Cultural heritage monuments in Malta with known IDs',
	P758  = 'Cultural heritage monuments in Norway with known IDs',
	P1770 = 'Cultural heritage monuments in Romania with known IDs',
	P1708 = 'Cultural heritage monuments in Saxony with known ID',
	P808  = 'Cultural heritage monuments in Spain by ID',
	P762  = 'Cultural monuments in the Czech Republic with known IDs',
	P477  = 'Heritage properties in Canada with known IDs',
	P5094 = 'HPIP with known IDs',
	P1702 = 'IGESPAR with known IDs',
	P5500 = 'IPHAN with known IDs',
	P2783 = 'Listed buildings in Denmark with known IDs',
	P1216 = 'Listed buildings in England with known IDs',
	P1460 = 'Listed buildings in Northern Ireland with known IDs',
	P709  = 'Listed buildings in Scotland with known IDs',
	P1459 = 'Listed buildings in Wales with known IDs',
	P649  = 'National Register of Historic Places with known IDs',
	P4120 = 'Ontario Heritage Trust sites with known IDs',
	P2961 = 'Periodicals in the Biblioteca Virtual de Prensa Histórica',
	P7135 = 'Rijksmonumentcomplexen with known IDs',
	P359  = 'Rijksmonumenten with known IDs',
	P1700 = 'SIPA with known IDs',
	P3759 = 'Uses of Wikidata Infobox providing SAHRA ids',
	P809  = 'Uses of Wikidata Infobox providing WDPA ids',
}

--- qualifiers for "headquarters location" (P159)
local hq_quals = table.concat({
	'P6375',  -- street address
	'P669',   -- located on street
	'P670',   -- street number
	'P4856',  -- conscription number
	'P281',   -- postal code
	'P580',   -- start time
	'P582',   -- end time
	'P585',   -- point in time
	'P1264',  -- valid in period
	'P3831',  -- object has role
	'P1810',  -- subject named as
	'P5102',  -- nature of statement
}, ',' )

--- associates pids with a table of arguments for WikidataIB or with a function
--- that will be called with pid as the only argument
local property_logic = {
	P51    = getAudio,                    -- audio
	P989   = getAudioByLang,              -- spoken text audio
	P443   = getAudioByLang,              -- pronunciation audio
	P990   = getAudioByLang,              -- recording of subject's voice
	P7383  = getHieroglyphs,              -- name in hiero markup
	P569   = getBirth,                    -- date of birth
	P570   = getDeath,                    -- date of death
	P69    = { qual='P580,P582,P585,P512,P812' }, -- educated at
	P185   = { collapse=4, maxvals=20 },  -- doctoral student
	P106   = defaultFuncMobileGendered,   -- occupation
	P39    = { qual='P642,P580,P582,P585', collapse=6 }, -- position held
	P2522  = { collapse=4 },              -- victory
	P26    = { qual='DATES' },            -- spouse, TODO: sort by date qualifier (also P793)
	P451   = { qual='DATES' },            -- partner
	P166   = { qual='P585' },             -- award received
	P856   = getWebsite,                  -- official website
	P109   = getSignature,                -- signature
	P31    = defaultFuncMobile,           -- instance of
	P2250  = getByCountry,                -- life expectancy
	P4841  = getByCountry,                -- total fertility rate
	P5236  = getPrimeFactors,             -- prime factor
	P487   = getUnicodeChars,             -- Unicode character
	P3295  = getCodes,                    -- code
	P7415  = getCodeImages,               -- code (image)
	P3270  = getByLocation,               -- compulsory education (minimum age)
	P3271  = getByLocation,               -- compulsory education (maximum age)
	P6897  = getByLocationCollapse4,      -- literacy rate
	P2573  = getByLocationCollapse4,      -- number of out-of-school children
	P971   = { osd='no' },                -- category combines topics
	P180   = { list='prose', qual='' },   -- depicts
	P276   = getLocation,                 -- location
	P50    = getAuthors,                  -- author
	P2789  = { qual='' },                 -- connects with
	P85    = { qual='DATES' },            -- anthem
	P953   = { qual='P407', prefix="[", postfix="]" }, -- full work at
	P127   = { qual='DATES' },            -- owned by
	P159   = { qual=hq_quals },           -- headquarters location
	P466   = { collapse=5 },              -- occupant
	P126   = { collapse=5, maxvals=20 },  -- maintained by
	P348   = { qual='P548,P577,P805' },   -- software version identifier
	P286   = { collapse=3 },              -- head couch
	P527   = { collapse=5, maxvals=20 },  -- has part
	P1382  = { collapse=5, maxvals=20 },  -- partially coincident with
	P1990  = { collapse=5 },              -- species kept
	P1923  = { collapse=5, maxvals=10 },  -- participating team
	P1346  = { collapse=5, maxvals=20 },  -- winner
	P112   = { maxvals=20 },              -- founded by
	P577   = {
		linked = 'no',             -- make film categories load much quicker
		rank = 'preferred normal', -- See [[d:Property_talk:P577#Constraint_about_unique_best_value]]
	},
	P1082  = { qual='P585' },             -- population (qual = point in time)
	P200   = { collapse=4, maxvals=20 },  -- lake inflows
	P205   = { collapse=5, maxvals=20 },  -- basin country
	P974   = { collapse=5, maxvals=20 },  -- tributary
	P726   = { collapse=5 },              -- candidate
	P1889  = getDifferentFrom,            -- different from
	P460   = { collapse=20, list='' },    -- same as (lots of values for given names)
	P1843  = getVernacularName,           -- taxon common name
	P171   = getTaxontree,                -- parent taxons
	P1403  = getOriginalCombination,      -- original combination
	P225   = getTaxonAuthor,              -- taxon name (and qualifiers)
	P2078  = getWebsite,                  -- user manual URL
	P625   = getCoordinates,              -- coordinate location
	P3896  = getCommonsMapData,           -- geoshape
	P6257  = getCelestialCoordinates,     -- right ascension
}

--[==[----------------------------------------------------------------------
This table is used by main() to generate the infobox and by doc() to
generate [[Template:Wikidata Infobox/doc/properties]].

* `humans_allowed` determines whether the group should be displayed if the
  item is a human (Q5) or a fictional human (Q15632617). It defaults to false.
* A group will only be displayed if `P31_allowed_values` contains the
  "instance of" (P31) value of the item or `P31_allowed_values` is not present.
* If `bypass_property_exists_check` is set to true, the infobox tries to fetch
  the values for each pid in the group, even if the item has no pid statement.
* `logic` can be a function that will be called with pid as the only argument.
  `logic` can also be a WikidataIB arguments table for defaultFunc.
]==]
local property_groups = {
	{ groupname = 'Switchable images', -- this group needs to be at index 1
	  comment = 'Users can switch between these images using [[MediaWiki:Gadget-Infobox.js|Gadget-Infobox.js]].',
	  humans_allowed = true,
	  pids = {'P2716','P3383','P18','P13146','P117','P8224','P1442','P1801','P4640','P4291','P3451','P5252','P2713','P8592','P11832','P8517','P5555','P5775','P7417','P9721','P3311','P7420','P7457','P8195','P1543','P996','P3030','P154','P2910','P41','P94','P4004','P158','P2425','P8766','P14','P1766','P15','P8512','P181','P207','P242','P1944','P1943','P1846','P1621','P367','P491','P6655','P10','P4896','P11101','P11702','P12565'},
	},
	{ groupname = 'Audio and hieroglyphs',
	  humans_allowed = true,
	  pids = {'P51','P989','P443','P990','P7383'},
	},
	{ groupname = 'Human',
	  P31_allowed_values = { 'Q5', 'Q15632617' },
      comment = 'If this group is shown, the other groups below are ignored.',
	  humans_allowed = true,
	  pids = {'P1559','P569','P570','P1196','P509','P157','P119','P742','P2031','P2032','P1317','P27','P1532','P551','P69','P184','P185','P106','P2416','P6087','P54','P108','P463','P102','P39','P101','P135','P66','P103','P97','P2962','P2522','P793','P53','P22','P25','P3373','P40','P26','P1038','P451','P937','P800','P1441','P166','P856','P109'},
	},
	{ groupname = 'Instance/subclass of',
	  pids = {'P31','P279'},
	},
	{ groupname = 'Health by region',
	  P31_allowed_values = { 'Q64027457' },
	  pids = {'P2250','P4841'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Natural number',
	  P31_allowed_values = { 'Q21199' },
	  pids = {'P5236','P487','P3295','P7415'},
	},
	{ groupname = 'Education by region',
	  P31_allowed_values = { 'Q64801076' },
	  pids = {'P3270','P3271','P6897','P2573'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'National economy',
	  P31_allowed_values = { 'Q6456916' },
	  pids = {'P38','P2299','P4010','P2131','P2132','P2219','P1279','P2134','P2855'},
	  bypass_property_exists_check = true,
	  logic = getByLocationCollapse4,
	},
	{ groupname = 'Miscellaneous 1',
	  pids = {'P361','P1639','P1269','P921','P629','P1559','P452','P7163','P971','P4224','P831','P2317','P138','P825','P417','P547','P180','P2596','P186','P136','P376','P3018','P7532'},
	},
	{ groupname = 'Location',
	  comment = 'The properties {{P|131}}, {{P|276}}, {{P|706}}, and {{P|17}} together produce a single infobox row.',
	  pids = {'P276'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Miscellaneous 2',
	  pids = {'P1001','P206','P5353','P4856','P6529','P9759','P6375','P669','P495','P1885','P149','P708','P2872','P16','P2789','P59','P65','P215','P223','P196','P36','P122','P194','P208','P209','P37','P85','P38','P35','P6','P210'},
	},
	{ groupname = 'Author',
	  comment = 'Will be displayed together with {{P|2093}}.',
	  pids = {'P50'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Miscellaneous 3',
	  pids = {'P655','P123','P1433','P84','P193','P170','P86','P676','P87','P61','P189','P98','P58','P110','P162','P175','P393','P291','P4647','P407','P2635','P437','P953','P275','P1441','P1080','P88','P6291','P199','P169','P366','P121','P127','P159','P466','P137','P126','P177','P2505','P144','P822','P115','P5138','P118','P505','P286','P527','P1454','P1990','P2522','P1427','P1444','P1923','P1132','P1346','P176','P1071','P617','P504','P532','P8047','P289','P426','P113','P114','P375','P619','P1145','P522','P664','P823','P5804','P57','P161','P195','P217','P178','P112','P400','P306','P1435','P814','P141','P348','P585','P606','P729','P730','P580','P571','P577','P1191','P5444','P575','P1619','P3999','P582','P576','P2669','P793','P516','P2957','P2109','P618','P128','P129','P111','P179'},
	},
	{ groupname = 'Quantities',
	  pids = {'P1093','P2067','P2261','P2262','P2049','P2386','P2043','P3157','P2583','P2048','P5524','P2808','P2144','P3439','P4183','P5141','P4552','P2660','P2659','P610','P559','P7309','P1082','P2052','P2217','P2046','P2044','P2050','P2047'},
	  logic = { unitabbr='yes' },
	},
	{ groupname = 'Miscellaneous 4',
	  pids = {'P140','P1083','P2351','P2324','P6801','P6855','P3032','P3137','P770','P1398','P167','P81','P197','P833','P834'},
	},
	{ groupname = 'Water',
	  pids = {'P885','P403','P200','P201','P4614','P205','P974','P4792','P4661','P469','P2673','P2674'},
	},
	{ groupname = 'Miscellaneous 5',
	  pids = {'P155','P156','P1365','P1366','P3730','P3729'},
	},
	{ groupname = 'Elections',
	  pids = {'P991','P726','P1831','P1867','P1868','P1697','P5043','P5045','P5044'},
	},
	{ groupname = 'Miscellaneous 6',
	  pids = {'P1590','P1120','P1446','P1339','P1092','P784','P783','P785','P786','P787','P788','P789','P183','P2130','P2769','P1174','P859','P218','P78','P238','P239','P1889','P460','P1382','P2010','P2009','P2033','P1531','P8193'},
	},
	{ groupname = 'Taxon common name',
	  comment = "Common names are taken from the item's label, sitelink, and {{P|1843}}.",
	  pids = {'P1843'},
	  bypass_property_exists_check = true,
	},
	{ groupname = 'Taxonomy',
	  pids = {'P171','P1403','P225'},
	},
	{ groupname = 'Miscellaneous 7',
	  pids = {'P6591','P7422','P2078','P856','P6257'},
	},
	{ groupname = 'Maps',
	  comment = '{{P|3896}} is only used if no {{P|625}} statement exists. Tracked at {{c|Uses of Wikidata Infobox with maps}}.',
	  pids = {'P625','P3896'},
	  bypass_property_exists_check = true,
	},
}

local externalIDs = {
	{ groupname = 'Authority control',
	  pids = {'P213','P214','P227','P244','P268','P269','P270','P349','P409','P508','P640','P651','P691','P886','P902','P906','P947','P949','P950','P1003','P1006','P1015','P1048','P1157','P1207','P1225','P1415','P1695','P2558','P2581','P4819','P5034','P5587','P7293','P8189','P9371','P10539',}
	},
	{ groupname = 'Books/magazines/authors/libraries',
	  pids = {'P236','P271','P396','P648','P723','P724','P2961','P5199',}
	},
	{ groupname = 'Science',
	  pids = {'P356','P496','P549','P698','P717','P932','P1053','P2349','P3083','P8273',}
	},
	{ groupname = 'Biology',
	  pids = {'P428','P627','P685','P687','P6535','P815','P830','P838','P842','P846','P850','P938','P959','P960','P961','P962','P1070','P1076','P1348','P1391','P1421','P1727','P1745','P1746','P1747','P1761','P1772','P1832','P1895','P1940','P1991','P1992','P2007','P2026','P2036','P2040','P2426','P2434','P2455','P2464','P2752','P2833','P2946','P3031','P3060','P3064','P3099','P3100','P3101','P3102','P3151','P3240','P3288','P3398','P3420','P3444','P3591','P3594','P3606','P3746','P4024','P4122','P4194','P4301','P4526','P4567','P4728','P4758','P4855','P5036','P5037','P5055','P5216','P5221','P5257','P5299','P6678','P7051',}
	},
	{ groupname = 'Art',
	  pids = {'P245','P347','P434','P650','P781','P1882','P1901','P3293','P3634','P4399','P4659','P4701','P5950','P6506','P6631','P7704','P8386','P9394',}
	},
	{ groupname = 'Culture',
	  pids = {'P345','P539','P1219','P1220','P1248','P1362','P6113','P6132','P12037',}
	},
	{ groupname = 'Sports',
	  pids = {'P1146','P1440','P1469','P1665','P2020','P2276','P2446','P2458','P2574','P3171','P3537','P3538','P3681','P3924','P8286',}
	},
	{ groupname = 'Cultural heritage and architecture',
	  pids = {'P359','P380','P381','P454','P481','P649','P709','P718','P757','P758','P762','P808','P1216','P1305','P1459','P1483','P1600','P1700','P1702','P1708','P1764','P1769','P2424','P2783','P2081','P2917','P3038','P3177','P3178','P3318','P3449','P3596','P3758','P3759','P4009','P4075','P4102','P4244','P4360','P4372','P4868','P5094','P5310','P5313','P5500','P5525','P5528','P6102','P6542','P6736','P7006','P7170','P7304','P7630','P7659','P7694','P7900','P9148','P9154','P9339','P9342','P10486','P11351','P11557'}
	},
	{ groupname = 'Protected areas',
	  pids = {'P809','P3425','P3613','P3974','P5965','P6602','P6230','P6280','P6478','P6560','P6659','P3296','P677',}
	},
	{ groupname = 'Places and geographical features',
	  pids = {'P402','P11693','P10689','P3120','P3580','P3616','P3628','P4266','P6630','P7350','P7352','P7548','P8655','P8988','P10451','P4533',}
	},
	{ groupname = 'Administrative subdivisions',
	  pids = {'P772','P836','P1894','P3118','P3615','P3639','P3419','P7526','P2788','P7577','P7606','P7635','P7636','P7579','P7752','P7673','P7674','P7736','P7735',}
	},
	{ groupname = 'Other',
	  pids = {'P458','P587','P2037','P3112','P10557','P3479','P4344','P6228','P7721',}
	},
}

--- @param group table
local function groupIsAllowed( group )
	local ishuman = INSTANCEOF['Q5'] or INSTANCEOF['Q15632617']
	if ishuman and not group.humans_allowed then return false end

	local allowlist = group.P31_allowed_values
	if not allowlist then return true end
	for _, class in ipairs( allowlist ) do
		if INSTANCEOF[class] then return true end
	end
	return false
end

local function noImage()
	-- Wikidata classes that don't need an image
	local dontNeedImg = {
		'Q4167410',  -- disambiguation page
		'Q4167836',  -- Wikimedia category
		'Q11266439', -- Wikimedia template
		'Q14204246', -- Wikimedia project page
		'Q13406463', -- Wikimedia list article
		'Q101352',   -- family name
		'Q202444',   -- given name
		'Q12308941', -- male given name
		'Q11879590', -- female given name
		'Q3409032',  -- unisex given name
	}
	for _, class in ipairs( dontNeedImg ) do
		if INSTANCEOF[class] then return end
	end

	local hasImg
	for _, imgPid in ipairs( property_groups[1].pids ) do
		if CLAIMS[imgPid] then
			hasImg = true
			break
		end
	end
	if not hasImg then
		return '[[Category:Uses of Wikidata Infobox with no image]]'
	end
end

--- Returns string with all labels/descs/aliases for search engine optimization
local function seo()
	local out = {}

	for lang, v in pairs( ITEM.labels or {} ) do
		out[#out+1] = v.value
	end

	for lang, v in pairs( ITEM.descriptions or {} ) do
		out[#out+1] = v.value
	end

	for lang, v in pairs( ITEM.aliases or {} ) do
		for _, w in ipairs( v ) do
			out[#out+1] = w.value
		end
	end

	return table.concat( out, '; ' )
end

-- wikiprojects that are not Wikipedia despite their IDs ending with 'wiki'
local excludedProjects = {
	wikidatawiki = true, commonswiki   = true, specieswiki   = true,
	metawiki     = true, mediawikiwiki = true, outreachwiki  = true,
	sourceswiki  = true, wikimaniawiki = true, incubatorwiki = true,
	akwiki       = true, foundationwiki = true, wikifunctionswiki = true,
}

-- Returns interwiki link if site is Wikipedia
local function interwikilink( site, title )
	-- from LanguageCode.php
	local deprecatedLangs = {
		['als']          = 'gsw',       -- T25215
		['bat-smg']      = 'sgs',       -- T27522
		['be-x-old']     = 'be-tarask', -- T11823
		['fiu-vro']      = 'vro',       -- T31186
		['roa-rup']      = 'rup',       -- T17988
		['zh-classical'] = 'lzh',       -- T30443
		['zh-min-nan']   = 'nan',       -- T30442
		['zh-yue']       = 'yue',       -- T30441
	}
	if site:sub(-4) == 'wiki' and not excludedProjects[site] then
		local iwprefix = site:sub(1, -5):gsub('_', '-') -- "zh_yuewiki" to "zh-yue"
		if deprecatedLangs[iwprefix] then
			iwprefix = deprecatedLangs[iwprefix]
		end
		return string.format( '[[%s:%s]]', iwprefix, title )
	end
end

--- Adds Wikipedia sitelinks from similar items. Example at Cat:Moore_(surname)
local function interwikis()
	local out = {}

	-- ITEM is usually P301 of connected item, so this is not redundant:
	for site, v in pairs( ITEM.sitelinks or {} ) do
		out[#out+1] = interwikilink( site, v.title )
	end

	for _, pid in ipairs{ 'P910', 'P2354', 'P1753', 'P460', 'P1420' } do -- topic's main category, has list, related list, said to be same as, taxon synonym
		for similar in iclaims( ITEM:getBestStatements(pid) ) do
			for site, v in pairs( mw.wikibase.getEntity(similar.id).sitelinks or {} ) do
				out[#out+1] = interwikilink( site, v.title )
			end
		end
	end

	return table.concat( out )
end

local charMap -- memoized
local function stripDiacritics( str )
	if not charMap then
		local from = 'ÁÀÂÄǍĂĀÃÅẠĄƏĆĊĈČÇĎĐḐḌÐÉÈĖÊËĚĔƐƎỀỂỄẾỆĒẼĘẸĠĜĞĢĤĦḤİÍÌÎÏǏĬĪĨĮỊĴĶĹĿĽĻŁḶḸṂŃŇÑŅṆŊÓÒÔÖǑŎŌÕǪỌŐØꝚŔŘŖⱤɌƦȐȒṘṚṜŚŜŠŞȘṢŤŢȚṬÚÙÛÜǓŬŪŨŮŲỤŰǗǛǙǕŴÝŶŸỸȲŹŻŽ'..
		             'ằắắáẳàẵâäǎăāãåặầẩẫấậảạąəćċĉčçḑďđḍðéèėêëěɛǝềểễếệĕēẽęẹġĝğģḩĥħḥıíìîïǐĭīĩįịĵķĺŀľļłḷḹṃńňñņṇŋơóồòôöǒŏōõǫọőøꝛŕɽřŗṛṝɍʀȑȓṙśŝšşșṣťţțṭưúùûứừüǔŭūũůųụűǘǜǚǖŵýŷÿỹȳźżž'
		local to   = 'AAAAAAAAAAAACCCCCDDDDDEEEEEEEEEEEEEEEEEEGGGGHHHIIIIIIIIIIIJKLLLLLLLMNNNNNNOOOOOOOOOOOORRRRRRRRRRRRSSSSSSTTTTUUUUUUUUUUUUUUUUWYYYYYZZZ'..
		             'aaaaaaaaaaaaaaaaaaaaaaaacccccdddddeeeeeeeeeeeeeeeeeegggghhhhiiiiiiiiiiijklllllllmnnnnnnoooooooooooooorrrrrrrrrrrrssssssttttuuuuuuuuuuuuuuuuuuuwyyyyyzzz'
		charMap = {}
		for i = 1, mw.ustring.len( from ) do
			charMap[mw.ustring.sub(from, i, i)] = mw.ustring.sub(to, i, i)
		end
		charMap['ß'] = 'ss'; charMap['ẞ'] = 'SS'
		charMap['æ'] = 'ae'; charMap['ǣ'] = 'ae'; charMap['ǽ'] = 'ae'
		charMap['Æ'] = 'AE'; charMap['Ǣ'] = 'AE'; charMap['Ǽ'] = 'AE'
		charMap['œ'] = 'oe'; charMap['Œ'] = 'OE'
		charMap['þ'] = 'th'; charMap['Þ'] = 'Th'
	end

	return (string.gsub( str, '[^\128-\191][\128-\191]*', charMap ))
end

local function humannames( out )
	local surname    = ITEM:formatPropertyValues('P734').value:gsub(',.*', '')
	local givennames = ITEM:formatPropertyValues('P735').value:gsub(', ', ' ')
	local spanish2nd = ITEM:formatPropertyValues('P1950').value:gsub(',.*', '')

	if config.trackingcats then
		if surname == '' then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no family name]]'
		end
		if givennames == '' then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no given name]]'
		end
	end

	if config.autocat then
		for _, pid in ipairs{ 'P734', 'P1950', 'P9139' } do
			for surname in iclaims( ITEM:getBestStatements(pid) ) do
				local sitelink = getCommonsLink( surname.id )
				if sitelink and sitelink:sub(1,9) == 'Category:' then
					if givennames == '' then
						out[#out+1] = string.format('[[%s]]', sitelink)
					else
						out[#out+1] = string.format('[[%s|%s]]', sitelink, stripDiacritics(givennames))
					end
				else
					local surnamelabel = mw.wikibase.getLabelByLang( surname.id, 'en' ) or mw.wikibase.getLabelByLang( surname.id, 'mul' )
					if givennames == '' then
						out[#out+1] = surnamelabel and string.format('[[Category:%s (surname)]][[Category:Uses of Wikidata Infobox with no surname sitelink]]', surnamelabel)
					else
						out[#out+1] = surnamelabel and string.format('[[Category:%s (surname)|%s]][[Category:Uses of Wikidata Infobox with no surname sitelink]]', surnamelabel, stripDiacritics(givennames))
					end
				end
			end
		end

		for givenname in iclaims( ITEM:getBestStatements('P735') ) do
			local sitelink = getCommonsLink( givenname.id )
			if sitelink and sitelink:sub(1,9) == 'Category:' then
				out[#out+1] = string.format('[[%s]]', sitelink)		-- no sort key needed because DEFAULTSORT starts with family name
			else
				local givennamelabel = mw.wikibase.getLabelByLang( givenname.id, 'en' ) or mw.wikibase.getLabelByLang( givenname.id, 'mul' )
				out[#out+1] = givennamelabel and string.format('[[Category:%s (given name)]]', givennamelabel)
			end
		end
	end

	if not config.defaultsort then
		out[#out+1] = '[[Category:Uses of Wikidata Infobox with defaultsort suppressed]]'
	elseif surname ~= '' and surname ~= 'no value' and surname ~= 'some value' then
		if spanish2nd ~= '' then
			surname = surname .. ' ' .. spanish2nd
		end
		local sortkey = stripDiacritics( surname..', '..givennames )
		out[#out+1] = frame:preprocess('{{DEFAULTSORT:'..sortkey..'}}')
	end
end

--- @param pid "P569"|"P570"
--- @param event "birth"|"death"
local function datecat( pid, event, out )
	local year = WikidataIB._getValue{ pid, qid=QID, ps=1, df='y', plaindate='adj', lang='en', maxvals=1 }
	if year and year ~= 'unknown value' then
		local cat = 'Category:' .. year .. ' ' .. event .. 's'
		if mw.title.new( cat ).exists then
			out[#out+1] = '[['..cat..']]'
		elseif config.trackingcats then
			mw.addWarning( 'Categorization under [[:'..cat..']] supressed' )
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with unknown '..event..' category|'..year..']]'
		end
	end
end

local function countrycat( out )
	local countryLabels = {
		-- All countries that have a "Men of {} by name" Commons category.
		-- When adding a value, make sure it's not a [[Template:Category redirect]].
		-- See also [[User:LennardHofmann/countries without "men by name" category]].
		Q16 = "Canada",
		Q17 = "Japan",
		Q20 = "Norway",
		Q21 = "England",
		Q22 = "Scotland",
		Q25 = "Wales",
		Q26 = "Northern Ireland",
		Q27 = "the Republic of Ireland",
		Q28 = "Hungary",
		Q29 = "Spain",
		Q30 = "the United States",
		Q31 = "Belgium",
		Q32 = "Luxembourg",
		Q33 = "Finland",
		Q34 = "Sweden",
		Q35 = "Denmark",
		Q36 = "Poland",
		Q37 = "Lithuania",
		Q38 = "Italy",
		Q39 = "Switzerland",
		Q40 = "Austria",
		Q41 = "Greece",
		Q43 = "Turkey",
		Q45 = "Portugal",
		Q55 = "the Netherlands",
		Q77 = "Uruguay",
		Q79 = "Egypt",
		Q96 = "Mexico",
		Q114 = "Kenya",
		Q115 = "Ethiopia",
		Q117 = "Ghana",
		Q142 = "France",
		Q145 = "the United Kingdom",
		Q148 = "the People's Republic of China",
		Q155 = "Brazil",
		Q159 = "Russia",
		Q183 = "Germany",
		Q184 = "Belarus",
		Q189 = "Iceland",
		Q191 = "Estonia",
		Q211 = "Latvia",
		Q212 = "Ukraine",
		Q213 = "the Czech Republic",
		Q214 = "Slovakia",
		Q215 = "Slovenia",
		Q217 = "Moldova",
		Q218 = "Romania",
		Q219 = "Bulgaria",
		Q221 = "North Macedonia",
		Q222 = "Albania",
		Q223 = "Greenland",
		Q224 = "Croatia",
		Q225 = "Bosnia and Herzegovina",
		Q227 = "Azerbaijan",
		Q228 = "Andorra",
		Q229 = "Cyprus",
		Q230 = "Georgia",
		Q232 = "Kazakhstan",
		Q233 = "Malta",
		Q235 = "Monaco",
		Q236 = "Montenegro",
		Q238 = "San Marino",
		Q241 = "Cuba",
		Q242 = "Belize",
		Q244 = "Barbados",
		Q252 = "Indonesia",
		Q258 = "South Africa",
		Q262 = "Algeria",
		Q265 = "Uzbekistan",
		Q298 = "Chile",
		Q334 = "Singapore",
		Q347 = "Liechtenstein",
		Q398 = "Bahrain",
		Q399 = "Armenia",
		Q403 = "Serbia",
		Q408 = "Australia",
		Q414 = "Argentina",
		Q419 = "Peru",
		Q423 = "North Korea",
		Q424 = "Cambodia",
		Q574 = "East Timor",
		Q657 = "Chad",
		Q664 = "New Zealand",
		Q668 = "India",
		Q672 = "Tuvalu",
		Q678 = "Tonga",
		Q683 = "Samoa",
		Q685 = "the Solomon Islands",
		Q686 = "Vanuatu",
		Q691 = "Papua New Guinea",
		Q695 = "Palau",
		Q697 = "Nauru",
		Q702 = "the Federated States of Micronesia",
		Q709 = "the Marshall Islands",
		Q711 = "Mongolia",
		Q712 = "Fiji",
		Q717 = "Venezuela",
		Q730 = "Suriname",
		Q733 = "Paraguay",
		Q734 = "Guyana",
		Q736 = "Ecuador",
		Q739 = "Colombia",
		Q750 = "Bolivia",
		Q754 = "Trinidad and Tobago",
		Q757 = "Saint Vincent and the Grenadines",
		Q760 = "Saint Lucia",
		Q763 = "Saint Kitts and Nevis",
		Q766 = "Jamaica",
		Q769 = "Grenada",
		Q774 = "Guatemala",
		Q781 = "Antigua and Barbuda",
		Q783 = "Honduras",
		Q784 = "Dominica",
		Q786 = "the Dominican Republic",
		Q790 = "Haiti",
		Q792 = "El Salvador",
		Q794 = "Iran",
		Q796 = "Iraq",
		Q800 = "Costa Rica",
		Q801 = "Israel",
		Q804 = "Panama",
		Q805 = "Yemen",
		Q810 = "Jordan",
		Q811 = "Nicaragua",
		Q813 = "Kyrgyzstan",
		Q817 = "Kuwait",
		Q819 = "Laos",
		Q822 = "Lebanon",
		Q826 = "the Maldives",
		Q833 = "Malaysia",
		Q836 = "Myanmar",
		Q837 = "Nepal",
		Q842 = "Oman",
		Q843 = "Pakistan",
		Q846 = "Qatar",
		Q851 = "Saudi Arabia",
		Q854 = "Sri Lanka",
		Q858 = "Syria",
		Q863 = "Tajikistan",
		Q865 = "Taiwan",
		Q869 = "Thailand",
		Q874 = "Turkmenistan",
		Q878 = "the United Arab Emirates",
		Q881 = "Vietnam",
		Q884 = "South Korea",
		Q889 = "Afghanistan",
		Q902 = "Bangladesh",
		Q912 = "Mali",
		Q916 = "Angola",
		Q917 = "Bhutan",
		Q921 = "Brunei",
		Q924 = "Tanzania",
		Q928 = "the Philippines",
		Q929 = "the Central African Republic",
		Q945 = "Togo",
		Q948 = "Tunisia",
		Q953 = "Zambia",
		Q954 = "Zimbabwe",
		Q958 = "South Sudan",
		Q962 = "Benin",
		Q963 = "Botswana",
		Q965 = "Burkina Faso",
		Q967 = "Burundi",
		Q970 = "the Comoros",
		Q971 = "the Republic of the Congo",
		Q974 = "the Democratic Republic of the Congo",
		Q977 = "Djibouti",
		Q983 = "Equatorial Guinea",
		Q986 = "Eritrea",
		Q1000 = "Gabon",
		Q1005 = "the Gambia",
		Q1006 = "Guinea",
		Q1007 = "Guinea-Bissau",
		Q1008 = "Ivory Coast",
		Q1009 = "Cameroon",
		Q1011 = "Cape Verde",
		Q1013 = "Lesotho",
		Q1014 = "Liberia",
		Q1016 = "Libya",
		Q1019 = "Madagascar",
		Q1020 = "Malawi",
		Q1025 = "Mauritania",
		Q1027 = "Mauritius",
		Q1028 = "Morocco",
		Q1029 = "Mozambique",
		Q1030 = "Namibia",
		Q1032 = "Niger",
		Q1033 = "Nigeria",
		Q1036 = "Uganda",
		Q1037 = "Rwanda",
		Q1039 = "São Tomé and Príncipe",
		Q1041 = "Senegal",
		Q1042 = "Seychelles",
		Q1044 = "Sierra Leone",
		Q1045 = "Somalia",
		Q1049 = "Sudan",
		Q1050 = "Eswatini",
		Q1055 = "Hamburg",
		Q1183 = "Puerto Rico",
		Q1209 = "Bremen",
		Q1231 = "Kosovo",
		Q1246 = "Kosovo",
		Q1410 = "Gibraltar",
		Q1741 = "Vienna",
		Q2184 = "the Russian Soviet Federative Socialist Republic",
		Q4948 = "the Republic of Venice",
		Q6250 = "Western Sahara",
		Q7318 = "Nazi Germany",
		Q8646 = "Hong Kong",
		Q8680 = "the British Empire",
		Q11768 = "Ancient Egypt",
		Q12548 = "the Holy Roman Empire",
		Q12560 = "the Ottoman Empire",
		Q14773 = "Macau",
		Q15180 = "the Soviet Union",
		Q15581 = "the Principality of Catalonia",
		Q15864 = "the United Kingdom of the Netherlands",
		Q16641 = "American Samoa",
		Q16957 = "the German Democratic Republic",
		Q17557 = "California Republic",
		Q20135 = "the Grand Duchy of Hesse",
		Q22502 = "Taiwan",
		Q22880 = "Electoral Palatinate",
		Q23334 = "Abkhazia",
		Q23681 = "Northern Cyprus",
		Q25230 = "the Bailiwick of Guernsey",
		Q26988 = "the Cook Islands",
		Q27306 = "the Kingdom of Prussia",
		Q28513 = "Austria-Hungary",
		Q29999 = "the Kingdom of the Netherlands",
		Q31747 = "the Irish Free State",
		Q33946 = "Czechoslovakia",
		Q34020 = "Niue",
		Q34266 = "the Russian Empire",
		Q34762 = "the Principality of Bayreuth",
		Q35672 = "the Pitcairn Islands",
		Q36704 = "Yugoslavia",
		Q37024 = "Serbia and Montenegro",
		Q38610 = "the Dominion of Newfoundland",
		Q38872 = "Prussia",
		Q41304 = "the Weimar Republic",
		Q42199 = "the Principality of Ansbach",
		Q42585 = "the Kingdom of Bohemia",
		Q43287 = "the German Empire",
		Q45670 = "the Kingdom of Portugal",
		Q46370 = "West Francia",
		Q47261 = "the Duchy of Bavaria",
		Q49683 = "the Grand Duchy of Lithuania",
		Q56036 = "West Berlin",
		Q58296 = "the French First Republic",
		Q61292 = "the Far Eastern Republic",
		Q62589 = "the Union between Sweden and Norway",
		Q62633 = "the Grand Principality of Finland",
		Q69829 = "French Fourth Republic",
		Q70802 = "French Third Republic",
		Q70972 = "the Kingdom of France",
		Q71084 = "the First French Empire",
		Q71747 = "the Duchy of Brittany",
		Q80702 = "the Spanish Empire",
		Q83286 = "Socialist Federal Republic of Yugoslavia",
		Q121932 = "the People's Republic of Bulgaria",
		Q124943 = "the Kingdom of Egypt",
		Q129286 = "British Raj",
		Q130280 = "Estonian Soviet Socialist Republic",
		Q131964 = "the Austrian Empire",
		Q133356 = "Ukrainian Soviet Socialist Republic",
		Q139319 = "the Russian Republic",
		Q140359 = "First Czechoslovak Republic",
		Q146562 = "the State of Damascus",
		Q146600 = "the Arab Kingdom of Syria",
		Q146880 = "the State of Syria",
		Q146885 = "Second Syrian Republic",
		Q147909 = "the Kingdom of Bulgaria",
		Q148499 = "the Margraviate of Brandenburg",
		Q148540 = "the Republic of Florence",
		Q149805 = "the Kingdom of Iraq",
		Q150981 = "the North German Confederation",
		Q151624 = "German Confederation",
		Q152115 = "the Duchy of Warsaw",
		Q152750 = "the Protectorate of Bohemia and Moravia",
		Q152855 = "the Kingdom of Tungning",
		Q153015 = "the Kingdom of Saxony",
		Q153080 = "East Francia",
		Q153128 = "the Independent State of Croatia",
		Q153136 = "Habsburg monarchy",
		Q153529 = "the Duchy of Milan",
		Q153943 = "the Kingdom of Westphalia",
		Q154195 = "the Kingdom of Bavaria",
		Q154741 = "the Confederation of the Rhine",
		Q154849 = "the Grand Duchy of Tuscany",
		Q155019 = "the Duchy of Lorraine",
		Q156038 = "the Duchy of Courland and Semigallia",
		Q156199 = "the Electorate of Saxony",
		Q156418 = "the Kingdom of Hawaiʻi",
		Q157367 = "Brandenburg-Prussia",
		Q158445 = "the Grand Duchy of Mecklenburg-Schwerin",
		Q158835 = "the Prince-Bishopric of Liège",
		Q159631 = "the Kingdom of Württemberg",
		Q159856 = "the Duchy of Brabant",
		Q161036 = "the Free State of Prussia",
		Q161215 = "the Grand Duchy of Mecklenburg-Strelitz",
		Q161885 = "the Kingdom of Great Britain",
		Q164079 = "the Kingdom of Hanover",
		Q165154 = "the Kingdom of Sardinia",
		Q170072 = "Dutch Republic",
		Q170174 = "Papal States",
		Q170468 = "United Arab Republic",
		Q170588 = "the Republic of Texas",
		Q170604 = "New France",
		Q171150 = "the Kingdom of Hungary",
		Q172107 = "Polish–Lithuanian Commonwealth",
		Q172579 = "the Kingdom of Italy",
		Q173065 = "the Kingdom of Naples",
		Q174193 = "the United Kingdom of Great Britain and Ireland",
		Q174306 = "the Republic of Genoa",
		Q175276 = "the Kingdom of León",
		Q176495 = "the Federal State of Austria",
		Q178085 = "the Principality of Zeta",
		Q179293 = "the Kingdom of Castile",
		Q179876 = "the Kingdom of England",
		Q180393 = "the Kingdom of the Two Sicilies",
		Q185682 = "French Indochina",
		Q186096 = "the Tsardom of Russia",
		Q186320 = "the Grand Duchy of Baden",
		Q188553 = "the Batavian Republic",
		Q188586 = "the Kingdom of Sicily",
		Q188712 = "the Empire of Japan",
		Q191077 = "the Kingdom of Yugoslavia",
		Q193619 = "the Union of South Africa",
		Q199442 = "the Kingdom of Aragon",
		Q199821 = "Gran Colombia",
		Q200464 = "the Portuguese Empire",
		Q203493 = "the Kingdom of Romania",
		Q204920 = "the Crown of Aragon",
		Q205662 = "Tokugawa shogunate",
		Q207272 = "the Second Polish Republic",
		Q207353 = "Acadia",
		Q208169 = "the Republic of Ragusa",
		Q209065 = "the Kingdom of Greece",
		Q209857 = "the Kingdom of Lombardy–Venetia",
		Q211274 = "the Polish People's Republic",
		Q212278 = "the Kingdom of Holland",
		Q215443 = "the Swedish Empire",
		Q215530 = "the Kingdom of Ireland",
		Q218023 = "Orange Free State",
		Q219060 = "the State of Palestine",
		Q221457 = "Congress Poland",
		Q223936 = "the Kingdom of Italy",
		Q230791 = "the Kingdom of Scotland",
		Q241748 = "the Kingdom of Serbia",
		Q243610 = "Ukrainian People's Republic",
		Q244165 = "the Republic of Artsakh",
		Q253094 = "the Kingdom of Hungary",
		Q256961 = "the Electorate of Bavaria",
		Q258532 = "British America",
		Q268970 = "the Republic of German-Austria",
		Q284667 = "the Electorate of Mainz",
		Q310293 = "the Saar Protectorate",
		Q310650 = "Schaumburg-Lippe",
		Q330362 = "the Commonwealth of England",
		Q389004 = "Wallachia",
		Q389688 = "the Achaemenid Empire",
		Q391980 = "the Free Territory of Trieste",
		Q430309 = "Đại Việt",
		Q435583 = "the Old Swiss Confederacy",
		Q457167 = "West Ukrainian People's Republic",
		Q467864 = "the People's Socialist Republic of Albania",
		Q514423 = "the Parthenopean Republic",
		Q518101 = "the First Republic of Austria",
		Q529605 = "the Electorate of Hesse",
		Q533534 = "Cisleithania",
		Q556263 = "the Duchy of Brunswick-Lüneburg",
		Q574644 = "the Prince-Bishopric of Speyer",
		Q577867 = "the Kingdom of Poland",
		Q590743 = "Ruanda-Urundi",
		Q600018 = "the Kingdom of Hungary",
		Q600093 = "Habsburg Spain",
		Q618399 = "the Republic of the Congo (Léopoldville)",
		Q630882 = "the Republic of New Granada",
		Q637238 = "the Electorate of Baden",
		Q650370 = "the Margraviate of Baden-Durlach",
		Q650489 = "the Margraviate of Baden",
		Q654342 = "the Federation of Rhodesia and Nyasaland",
		Q655621 = "the Principality of Transylvania",
		Q682318 = "Swedish Pomerania",
		Q684030 = "the Principality of Serbia",
		Q686965 = "Anhalt-Bernburg",
		Q690821 = "the Republic of Baden",
		Q693570 = "the Duchy of Ferrara",
		Q696241 = "the Khanate of Kokand",
		Q696908 = "the Kingdom of Poland",
		Q699964 = "the Archduchy of Austria",
		Q701614 = "the Archbishopric of Salzburg",
		Q704300 = "the Free City of Frankfurt",
		Q707767 = "the Prince-Bishopric of Utrecht",
		Q713750 = "West Germany",
		Q736727 = "the Republic of Siena",
		Q750583 = "Southern Rhodesia",
		Q756617 = "the Kingdom of Denmark",
		Q766501 = "the Duchy of Mantua",
		Q779011 = "the Principality of Montenegro",
		Q836680 = "the Duchy of Nassau",
		Q838261 = "the Federal Republic of Yugoslavia",
		Q838931 = "the Kingdom of Italy",
		Q858841 = "the Kingdom of Croatia",
		Q877875 = "Palmyrene Empire",
		Q899706 = "Peru–Bolivian Confederation",
		Q913828 = "the Republic of Prekmurje",
		Q958291 = "the United Principalities of Moldavia and Wallachia",
		Q964024 = "Moldavian Democratic Republic",
		Q1031430 = "Habsburg Netherlands",
		Q1048340 = "the Kingdom of Albania",
		Q1057542 = "the Republic of Hawaii",
		Q1063498 = "the United States of the Ionian Islands",
		Q1077630 = "the Septinsular Republic",
		Q1152126 = "the People's Republic of the Congo",
		Q1206012 = "the German Reich",
		Q1233672 = "the County of Barcelona",
		Q1235720 = "the Republic of Lucca",
		Q1290149 = "the Federal People's Republic of Yugoslavia",
		Q1365493 = "the Republic of Pisa",
		Q1421888 = "Ubangi-Shari",
		Q1470101 = "the Kingdom of Hungary",
		Q1508143 = "Ukrainian State",
		Q1541723 = "the County of Lippe",
		Q1615455 = "the Duchy of Mirandola",
		Q1649871 = "the Kingdom of Poland",
		Q1658411 = "the Raj of Sarawak",
		Q1747689 = "Ancient Rome",
		Q1794352 = "the Electorate of Trier",
		Q1900716 = "Margraviate Hachberg-Sausenberg",
		Q1900717 = "the Margraviate of Brandenburg-Küstrin",
		Q1917014 = "the County of Sicily",
		Q2002279 = "Portuguese Guinea",
		Q2010024 = "the Second Czechoslovak Republic",
		Q2017684 = "the French protectorate of Tunisia",
		Q2037576 = "the Republic of San Marco",
		Q2079909 = "the Province of Massachusetts Bay",
		Q2196956 = "the Kingdom of Norway",
		Q2208280 = "the People's Republic of Angola",
		Q2227570 = "the Duchy of Württemberg",
		Q2252973 = "the Duchy of Florence",
		Q2305208 = "the Russian Socialist Federative Soviet Republic",
		Q2335128 = "the Province of New Jersey",
		Q2396442 = "the Kingdom of Galicia and Lodomeria",
		Q2415003 = "the Kingdom of Serbia",
		Q2577303 = "the Kingdom of Sardinia",
		Q2670751 = "the Margraviate of Moravia",
		Q2719360 = "Duchy of Luxembourg",
		Q3113481 = "the Principality of Transylvania",
		Q3324486 = "the Prince-Bishopric of Montenegro",
		Q3456410 = "the Republic of Mulhouse",
		Q3503555 = "Palembang Sultanate",
		Q3755547 = "the Kingdom of Italy",
		Q4304392 = "the Russian State",
		Q4398229 = "the Russian State (1918–1920)",
		Q6581823 = "the Southern Netherlands",
		Q7842409 = "Trinidad and Tobago",
		Q8890160 = "the Kingdom of Poland",
		Q10957559 = "the Principality of Moldavia",
		Q11750128 = "the Kingdom of Poland",
		Q13426199 = "the Republic of China",
		Q14164803 = "the Prince-Bishopric of Fulda",
		Q15102440 = "the Kingdom of Serbs, Croats and Slovenes",
		Q15824804 = "the Kénédougou Kingdom",
		Q16056854 = "the Kingdom of Hungary",
		Q16550783 = "the Duchy of Anhalt",
		Q17059107 = "the Western Wei",
		Q18285930 = "the German Empire of 1848/1849",
		Q19872858 = "the Earldom of Ulster",
		Q19901436 = "Suriname",
		Q23366230 = "the Republic of Geneva",
		Q23498721 = "Gold Coast",
		Q25395037 = "the Kingdom of Hungary",
		Q30890672 = "the Kingdom of Sicily",
		Q55599391 = "the Kingdom of Sardinia",
		Q64576860 = "the Lordship of Bologna",
		Q112660052 = "the British India",
	}
	for country in iclaims( ITEM:getBestStatements('P27') ) do
		local countryLabel = countryLabels[country.id]
		if not countryLabel then
			if config.trackingcats then
				out[#out+1] = '[[Category:Uses of Wikidata Infobox with unknown country category|'..country['numeric-id']..']]'
			end
		else
			local sex = getSingleValue( ITEM, 'P21' )
			local sexLabel = sex and ({
					Q6581097  = 'Men',
					Q2449503  = 'Men',
					Q6581072  = 'Women',
					Q1052281  = 'Women',
			})[sex.id]
			if sexLabel then
				out[#out+1] = '[[Category:'..sexLabel..' of '..countryLabel..' by name]]'
			else
				out[#out+1] = '[[Category:People of '..countryLabel..' by name]]'
			end
		end
	end
end

local function autocat( out, pid, dict )
	for _, claim in ipairs( ITEM:getAllStatements(pid) ) do
		if claim.rank ~= "deprecated" then
			local dv = claim.mainsnak.datavalue
			local cat = dict[dv and dv.value.id]
			out[#out+1] = cat and '[[Category:'..cat..']]'
		end
	end
end

local function metadata()
	local out = {}

	if config.trackingcats then
		out[#out+1] = noImage()
		if not (CLAIMS['P31'] or CLAIMS['P279']) then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no instance of]]'
		end
		if INSTANCEOF['Q5'] and not CLAIMS['P569'] then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no year of birth]]'
		elseif INSTANCEOF['Q4167836'] and not (CLAIMS['P301'] or CLAIMS['P971']) then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox with no topic]]'
		end
	end

	out[#out+1] = '<div style="display:none"><nowiki>'..seo()..'</nowiki></div>'

	-- Add interwiki links from related items, inspired by Module:Interwiki
	if config.interwiki and mw.title.getCurrentTitle().namespace == 14 then
		out[#out+1] = interwikis()
	end

	if config.autocat then
		for pid, cat in pairs( autocats_by_id ) do
			local val = getSingleValue( ITEM, pid )
			out[#out+1] = val and string.format( '[[Category:%s| %s]]', cat, val )
		end

		out[#out+1] = CLAIMS['P757'] and '[[Category:World Heritage Sites by name]]'

		autocat( out, 'P1435', {  -- heritage designation
			Q34932610 = 'Conjuntos de Interesse Municipal in Portugal by name',
			Q28419115 = 'Conjuntos de Interesse Público in Portugal by name',
			Q54171320 = 'Monuments under study in Portugal by name',
			Q15697324 = 'Imóveis de Interesse Público in Portugal by name',
			Q11791    = 'Imóveis de Interesse Municipal in Portugal by name',
			Q53806418 = 'Monuments included in classified sites in Portugal by name',
			Q28423275 = 'Monumentos de Interesse Municipal in Portugal by name',
			Q22222923 = 'Monumentos de Interesse Público in Portugal by name',
			Q908411   = 'Monumentos Nacionais in Portugal by name',
			Q28419400 = 'Sítios de Interesse Municipal in Portugal by name',
			Q28419109 = 'Sítios de Interesse Público in Portugal by name',
			Q54163210 = 'Pending classification monuments in Portugal by name',
		})


		autocat( out, 'P31', {  -- instance of
			Q235670   = 'Common years starting and ending on Sunday',
			Q235673   = 'Common years starting and ending on Saturday',
			Q235676   = 'Common years starting and ending on Wednesday',
			Q235680   = 'Common years starting and ending on Friday',
			Q235684   = 'Common years starting and ending on Tuesday',
			Q235687   = 'Common years starting and ending on Monday',
			Q235690   = 'Common years starting and ending on Thursday',
			Q217041   = 'Leap years starting on Sunday and ending on Monday',
			Q217026   = 'Leap years starting on Saturday and ending on Sunday',
			Q217015   = 'Leap years starting on Wednesday and ending on Thursday',
			Q217036   = 'Leap years starting on Friday and ending on Saturday',
			Q217034   = 'Leap years starting on Tuesday and ending on Wednesday',
			Q217024   = 'Leap years starting on Monday and ending on Tuesday',
			Q217019   = 'Leap years starting on Thursday and ending on Friday',
			Q66010119 = 'Months starting on Monday',
			Q66010126 = 'Months starting on Tuesday',
			Q66010132 = 'Months starting on Wednesday',
			Q66010139 = 'Months starting on Thursday',
			Q66010148 = 'Months starting on Friday',
			Q66010153 = 'Months starting on Saturday',
			Q66010158 = 'Months starting on Sunday',
			Q3305213  = 'Individual painting categories',
		})

		if INSTANCEOF['Q5'] and mw.title.getCurrentTitle().namespace == 14 then
			humannames( out )
			datecat( 'P569', 'birth', out )
			datecat( 'P570', 'death', out )
			countrycat( out )

			autocat( out, 'P21', {  -- sex or gender
				Q6581097  = 'Men by name',
				Q6581072  = 'Women by name',
				Q1052281  = 'LGBT people by name]][[Category:Women by name',
				Q2449503  = 'LGBT people by name]][[Category:Men by name',
				Q48270    = 'Non-binary people by name',
				Q12964198 = 'LGBT people by name', -- genderqueer
				Q1097630  = 'LGBT people by name', -- intersex
				Q18116794 = 'LGBT people by name', -- genderfluid
				Q505371   = 'LGBT people by name', -- agender
			})

			autocat( out, 'P509', {  -- cause of death
				Q2840     = "Deaths from influenza",
				Q8277     = "Deaths from multiple sclerosis",
				Q9687     = "Deaths from road accidents",
				Q11081    = "Deaths from Alzheimer's disease",
				Q11085    = "Deaths from Parkinson's disease",
				-- Q12078    = "Deaths from cancer", -- too unspecific
				Q12090    = "Deaths from cholera",
				Q12152    = "Deaths from myocardial infarction",
				Q12156    = "Deaths from malaria",
				Q12192    = "Deaths from pneumonia",
				Q12199    = "Deaths from AIDS",
				Q12202    = "Deaths from stroke",
				Q12204    = "Deaths from tuberculosis",
				Q12206    = "Deaths from diabetes",
				Q12214    = "Deaths from smallpox",
				Q12796    = "Deaths by gunshot",
				Q29496    = "Deaths from leukemia",
				Q36956    = "Deaths from leprosy",
				Q40867    = "Deaths by poisoning",
				Q41083    = "Deaths from syphilis",
				Q41571    = "Deaths from epilepsy",
				Q47790    = "Deaths from tetanus",
				Q47912    = "Deaths from lung cancer",
				Q48143    = "Deaths from meningitis",
				Q83030    = "Deaths from dementia",
				Q83319    = "Deaths from typhoid fever",
				Q128015   = "People executed by guillotine",
				Q128581   = "Deaths from breast cancer",
				Q131742   = "Deaths from hepatitis",
				Q133462   = "People who committed seppuku",
				Q133780   = "Deaths from plague (disease)",
				Q134649   = "Deaths from diphtheria",
				Q147778   = "Deaths from cirrhosis",
				Q152234   = "Deaths from edema",
				Q160105   = "Deaths from cervical cancer",
				Q160649   = "Deaths from typhus",
				Q172341   = "Deaths from ovarian cancer",
				Q175111   = "Death by hanging",
				Q178275   = "Deaths from Spanish flu",
				Q180614   = "Deaths from melanoma",
				Q181257   = "Deaths from prostate cancer",
				Q181754   = "Deaths from heart failure",
				Q183134   = "Deaths from sepsis",
				Q188605   = "Deaths from emphysema",
				Q188874   = "Deaths from colorectal cancer",
				Q189389   = "Deaths from aneurysm",
				Q189588   = "Deaths from stomach cancer",
				Q190564   = "Deaths from Huntington's disease",
				Q190805   = "Deaths from diseases and disorders of the heart",
				Q192102   = "Deaths from skin cancer",
				Q193840   = "Asphyxia",
				Q199804   = "Deaths from chronic obstructive pulmonary disease",
				Q200779   = "Deaths from genetic diseases and disorders",
				Q202837   = "Deaths from cardiac arrest",
				Q204933   = "People executed by decapitation",
				Q206901   = "Deaths from amyotrophic lateral sclerosis",
				Q208414   = "Deaths from lymphoma",
				Q210392   = "Military people killed in action",
				Q212961   = "Deaths from pancreatic cancer",
				Q220570   = "Deaths from pulmonary embolism",
				Q223102   = "Deaths from peritonitis",
				Q261327   = "Deaths from thrombosis",
				Q275466   = "Deaths from embolism",
				Q372701   = "Deaths from esophageal cancer",
				Q389735   = "Deaths from diseases and disorders of the cardiovascular system",
				Q401402   = "Deaths from nephritis",
				Q468455   = "People executed by burning",
				Q476921   = "Deaths from kidney failure",
				Q504775   = "Deaths from bladder cancer",
				Q506616   = "Deaths from drowning",
				Q621076   = "Self-immolation",
				Q623031   = "Deaths from liver cancer",
				Q707774   = "Deaths from coronary thrombosis",
				Q744913   = "Victims of aviation accidents or incidents",
				Q767485   = "Deaths from respiratory failure",
				Q809831   = "BASE jumping deaths",
				Q826522   = "Deaths from thyroid cancer",
				Q847583   = "Deaths from cardiomyopathy",
				Q852423   = "Deaths from laryngeal cancer",
				Q857667   = "Deaths from pulmonary edema",
				Q929737   = "Deaths from diseases and disorders of the liver",
				Q949302   = "Deaths from diseases and disorders of the skin",
				Q958797   = "Deaths from scleroderma",
				Q970208   = "Deaths from liver failure",
				Q977787   = "Deaths from gallbladder cancer",
				Q1036696  = "Deaths from hypothermia",
				Q1054718  = "Deaths from diseases and disorders of the kidneys",
				Q1193870  = "Deaths from multiple organ failure",
				Q1198391  = "Deaths from intracranial aneurysm",
				Q1209744  = "Deaths from uterine cancer",
				Q1368943  = "Deaths from cerebral hemorrhage",
				Q1649580  = "Deaths from organ failure",
				Q1963588  = "Deaths from diseases and disorders of the blood",
				Q2140674  = "Deaths by gunshot",
				Q2300099  = "Deaths from diseases and disorders of the digestive system",
				Q2509220  = "Deaths from blood cancer",
				Q2661443  = "Deaths from diseases and disorders of the endocrine system",
				Q2967712  = "Deaths by horse-riding accident",
				Q3010352  = "Deaths from diseases and disorders of the cerebrovascular system",
				Q3242950  = "Deaths from kidney cancer",
				Q3286546  = "Deaths from diseases and disorders of the respiratory system",
				Q3339235  = "Deaths from diseases and disorders of the nervous system",
				Q3392853  = "Deaths from diseases and disorders of the lungs",
				Q3505252  = "Deaths from drug overdose",
				-- Q3966286  = "Deaths from executions", -- too unspecific
				Q4941552  = "Deaths from diseases and disorders of the skeletal system",
				Q5526839  = "Deaths from gastrointestinal cancer",
				Q7130407  = "Deaths from diseases and disorders of the pancreas",
				Q7258523  = "Deaths in childbirth",
				Q7692360  = "Deaths from volcanic eruptions",
				Q7900883  = "Deaths from diseases and disorders of the genitourinary system",
				Q8084905  = "Deaths from autoimmune diseases and disorders",
				Q9303627  = "Deaths from brain cancer",
				Q14467705 = "Deaths from surgical complications",
				Q15747939 = "People executed by shooting",
				Q18123741 = "Deaths from infectious diseases and disorders",
				Q18554919 = "Deaths from bone cancer",
				Q19403959 = "Victims of rail transport accidents or incidents",
				Q55790434 = "Deaths from oral cancer",
				-- Q84263196 = "Deaths from COVID-19", -- has a subcategory for every country
			})

			out[#out+1] = '[[Category:People by name]]'
			out[#out+1] = CLAIMS['P570'] and '[[Category:Deceased people by name]]'
			out[#out+1] = WikidataIB.getAwardCat{ args = {qid=QID, fwd='ALL', osd=config.osd, noicon='yes'} }

			if not CLAIMS['P570'] then
				-- This person has no death date, but are they really alive?
				local birth = getSingleValue( ITEM, 'P569' )
				local year = tonumber( birth and birth.time:gsub('-.*', '') )
				if year and os.date('%Y') - year < 100 then
					out[#out+1] = '[[Category:Living people]]'
				end
			end
		end
	end

	return table.concat( out )
end

--- @return string|nil
local function getImage( pid )
	local claims = ITEM:getBestStatements( pid )
	local claim = getClaimByLang( claims, MYLANG ) or claims[1]
	local ms = claim and claim.mainsnak
	local file = ms and ms.datavalue and ms.datavalue.value

	if file then
		local panoramalink = (pid == 'P4640') and '|link=https://panoviewer.toolforge.org/#'..mw.uri.encode(file, 'WIKI') or ''
		local img = '<span class="wpImageAnnotatorControl wpImageAnnotatorCaptionOff">[[File:'..file..'|'..config.imagesize..panoramalink..']]</span>' -- equivalent to {{ImageNoteControl | caption=off | type=inline}}

		local medialegends = claim.qualifiers and claim.qualifiers['P2096']
		if medialegends then
			return img .. '<div>'..extractMonolingualText( medialegends )..'</div>'
		else
			return img -- no image caption
		end
	end
end

--- Returns images and sitelinks
--- @param uploadlink? boolean: Whether to show the "Upload media" link
local function header( uploadlink )
	local imgs = {}
	for _, imgPid in ipairs( property_groups[1].pids ) do
		local formatted_img = getImage(imgPid)
		imgs[#imgs+1] = formatted_img and { imgPid, formatted_img }
	end

	local switcherContainer = mw.html.create( 'div' )
	switcherContainer:addClass( 'switcher-container' )

	-- Only show switching labels if we have more than one image to show
	if #imgs > 1 then
		for _, img in ipairs( imgs ) do
			switcherContainer:tag( 'div' )
				:addClass( 'center' )
				:node( img[2] )
				:tag( 'span' )
					:attr{ class = "switcher-label", style = "display:none" }
					:node( '&nbsp;' .. getLabel(img[1]) .. '&nbsp;' )
		end
	elseif #imgs == 1 then
		switcherContainer:tag( 'div' )
			:addClass( 'center' )
			:node( imgs[1][2] )
	end

	local images = mw.html.create( 'tr' )
	images:tag( 'td' )
		:attr{ colspan=2, class="wdinfo_nomobile" }
		:css( 'text-align', 'center' )
		:tag( 'div' )
			:node( ITEM:getDescription() or '')
			:done()
		:node( switcherContainer )

	local out = {}

	if INSTANCEOF['Q4167410'] or INSTANCEOF['Q15407973'] then -- disambiguation page/category
		if config.trackingcats then
			out[1] = '[[Category:Uses of Wikidata Infobox for disambig pages]]'
		end
	elseif uploadlink then
		local url = tostring(mw.uri.fullUrl('Special:UploadWizard', {
			categories = mw.title.getCurrentTitle().text
		}))
		local text = mw.message.new('Cx-contributions-upload'):inLanguage(MYLANG):plain()
		out[1] = '<tr><td colspan=2 style="text-align:center"><b>['..url..' '..text..']</b></td></tr>'
	end

	local sitelinks = ITEM.sitelinks
	if config.sitelinks and sitelinks then
		out[#out+1] = '<tr><td colspan=2 style="text-align:center; font-weight:bold">'
		local langId = databaseId(MYLANG)
		local langprefix = langId:gsub('_', '-')

		local wikis = {
			-- wikiId,       prefix     logo,                qid,      multilang
			{ 'wiki',        '',        'Wikipedia-logo-v2', 'Q52',    false },
			{ 'wikiquote',   'q',       'Wikiquote-logo',    'Q369',   false },
			{ 'wikisource',  's',       'Wikisource-logo',   'Q263',   false },
			{ 'wikibooks',   'b',       'Wikibooks-logo',    'Q367',   false },
			{ 'wikinews',    'n',       'Wikinews-logo',     'Q964',   false },
			{ 'wikiversity', 'v',       'Wikiversity-logo',  'Q370',   false },
			{ 'specieswiki', 'species', 'Wikispecies-logo',  'Q13679', true  },
			{ 'wikivoyage',  'voy',     'Wikivoyage-logo',   'Q373',   false },
			{ 'metawiki',    'm',       'Wikimedia_Community_Logo', 'Q1063116', true },
		}

		for _, v in ipairs( wikis ) do
			local wikiId, prefix, logo, qid, multilang = unpack( v )
			logo = '[[File:'..logo..'.svg|16x16px|alt=|link=]]&nbsp;'
			if multilang then
				local sitelink = sitelinks[wikiId]
				if sitelink then
					out[#out+1] = '<div>'..logo..'[['..prefix..':'..sitelink.title..'|'..getLabel(qid)..']]</div>'
				end
			else
				local sitelink = sitelinks[langId .. wikiId]
				if sitelink then
					out[#out+1] = '<div>'..logo..'[['..prefix..':'..langprefix..':'..sitelink.title..'|'..getLabel(qid)..']]</div>'
				end
			end
		end
		out[#out+1] = '</td></tr>'
	end

	return tostring( images ) .. table.concat( out )
end

--- Returns "Edit at Wikidata" pencil
local function pencil()
	local msg, lang = i18n( 'editlink-alttext', FALLBACKLANGS )
	local out = mw.html.create( 'tr' )
	out
		:addClass( "wdinfo_nomobile" )
		:tag( 'td' )
			:css( 'text-align', 'right' )
			:attr{ lang = lang, colspan = 2 }
			:node( string.format('[[File:Blue pencil.svg|15px|link=d:%s|%s]]', QID, msg) )
	return tostring( out )
end

--- Evaluates all non-image property groups and adds generated HTML rows to
--- the table given as argument.
local function getBodyContent( t )
	for i, group in ipairs( property_groups ) do
		if i > 1 and groupIsAllowed( group ) then
			for _, pid in ipairs( group.pids ) do
				if CLAIMS[pid] or group.bypass_property_exists_check then
					local x = property_logic[pid] or group.logic or defaultFunc
					if type(x) == 'function' then
						t[#t+1] = x( pid )
					else -- type(x) == 'table'
						t[#t+1] = defaultFunc( pid, x )
					end
				end
			end
		end
	end
end

--- Returns the infobox's main content
local function body()
	if not CLAIMS then return '' end

	local out = {}
	getBodyContent( out )

	-- If category combines at most 2 topics, show subinfoboxes for those topics.
	-- See Category:Uses_of_Wikidata_Infobox_with_subinfoboxes
	local topics = ITEM:getBestStatements( 'P971' )
	if not topics or #topics > 2 then return table.concat( out ) end

	-- country (Q6256), continent (Q5107), sovereign state (Q3624078), ocean (Q9430)
	local geoEntities = { 'Q6256', 'Q5107', 'Q3624078', 'Q9430' }

	-- The loop below modifies these variables and restores them afterwards
	local qid, item, claims, istaxon, instanceof = QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF

	local map
	for _, claim in ipairs( topics ) do
		QID = claim.mainsnak.datavalue.value.id
		ITEM = mw.wikibase.getEntity( QID )
		if not ITEM then
			out[#out+1] = '[[Category:Uses of Wikidata Infobox for deleted Wikidata items]]'
			break
		end
		CLAIMS = ITEM.claims or {}
		ISTAXON = CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843']

		INSTANCEOF = {}
		for class in iclaims( ITEM:getBestStatements('P31') ) do
			INSTANCEOF[class.id] = true
		end

		local skip
		for _, geoEnt in ipairs( geoEntities ) do
			if INSTANCEOF[geoEnt] then
				skip = true
				map = getCoordinates( 'P625' )
				break
			end
		end

		-- Skip if topic is a calendar year (Q3186692) or decade (Q39911)
		skip = skip or INSTANCEOF['Q3186692'] or INSTANCEOF['Q39911']

		if not skip and #getBestStatements(QID, 'P279') == 0 then -- subclass of
			if config.trackingcats then
				out[#out+1] = '[[Category:Uses of Wikidata Infobox with subinfoboxes]]'
			end
			out[#out+1] = '<tr><th colspan=2>'..(ITEM:getLabel() or QID)..'</th></tr>'
			out[#out+1] = header( false )
			getBodyContent( out )
			out[#out+1] = pencil()
		end
	end
	out[#out+1] = map

	QID, ITEM, CLAIMS, ISTAXON, INSTANCEOF = qid, item, claims, istaxon, instanceof
	return table.concat( out )
end

local function authoritycontrol()
	if not config.authoritycontrol then return '' end

	local ids = {}
	for _, group in ipairs( externalIDs ) do
		for _, pid in ipairs( group.pids ) do
			if CLAIMS[pid] then
				local icon = getSingleValue( pid, 'P2910' )
				icon = icon and '[[File:'..icon..'|18px|alt=|link=]] ' or ''
				local fmtSt = ITEM:formatStatements( pid )
				if fmtSt.value ~= '' then
					ids[#ids+1] = icon .. fmtSt.label .. ': ' .. fmtSt.value
				end
			end
		end
	end

	local wdlogo = '[[File:Wikidata-logo.svg|20px|alt='..getLabel('Q2013')..'|link=d:'..QID..']]'
	return table.concat{
		'<tr><th style="background: #cfe3ff">',
			LANG:ucfirst( getLabel('Q36524') ),
		'</th></tr>',

		'<tr><td style="text-align: center;">',
			'<div style="overflow-wrap: break-word; font-size: smaller">',
				wdlogo..'&nbsp;[[d:'..QID..'|'..QID..']]<br>',
				'<span class="wdinfo_nomobile">',
					table.concat(ids, '<br>'),
				'</span>',
			'</div>',
		'</td></tr>',
	}
end

local function helperlinks()
	if not config.helperlinks then return '' end

	local hl = {}
	local title = mw.title.getCurrentTitle()
	local pagename = title.text
	local pagenamee = mw.uri.encode(pagename, 'WIKI')

	local coords = getSingleValue( ITEM, 'P625' )
	local otherplanet = coords and coords.globe ~= 'http://www.wikidata.org/entity/Q2'

	hl[#hl+1] = '[https://reasonator.toolforge.org/?q='..QID..' '..getLabel('Q20155952')..']'
	hl[#hl+1] = '[[toolforge:scholia/'..QID..'|'..getLabel('Q45340488')..']]'
	hl[#hl+1] = '[https://wikidocumentaries-demo.wmcloud.org/'..QID..' '..getLabel('Q85947706')..']'

	if title.namespace == 14 then
		hl[#hl+1] = '[https://petscan.wmflabs.org/?language=commons&categories='..pagenamee..'&project=wikimedia&ns%5B6%5D=1 '..getLabel('Q23665536')..']'
		hl[#hl+1] = '[https://glamtools.toolforge.org/glamorgan.html?&category='..pagenamee..'&depth=1&month=last '..getLabel('Q12483')..']'
		if not otherplanet then
			hl[#hl+1] = '[https://wikimap.toolforge.org/?cat='..pagenamee..'&subcats=true&subcatdepth=1&cluster=true '..getLabel('Q99232292')..']'
			hl[#hl+1] = '[https://locator-tool.toolforge.org/#/geolocate?category='..pagenamee..' '..getLabel('Q66498380')..']'
		end
	end

	hl[#hl+1] = '[https://kmlexport.toolforge.org/?project=commons&article='..mw.uri.encode(title.prefixedText)..' '..getLabel('P3096')..']'

	if coords and not otherplanet then
		hl[#hl+1] = '[https://wikishootme.toolforge.org/#q='..QID..'&main_commons_category='..pagenamee..' '..getLabel('Q26964791')..']'
		hl[#hl+1] = '[https://overpass-api.de/api/interpreter?data='..mw.uri.encode('[out:custom];rel[wikidata='..QID..'];if(count(relations)==0){way[wikidata='..QID..'];if(count(ways)==0){node[wikidata='..QID..'];};};out 1;', 'PATH')..' '..getLabel('Q936')..']'
	end

	for i, v in ipairs( hl ) do
		hl[i] = '<span style="white-space:nowrap">' .. v .. '</span>'
	end

	hl[#hl+1] = '[[Special:Search/haswbstatement:P180='..QID..'|'..i18n('search-depicted', FALLBACKLANGS)..']]'
	hl[#hl+1] = ISTAXON and '[https://commons-query.wikimedia.org/#%23defaultView%3AImageGrid%0ASELECT%20%3Ffile%20%3Fimage%0AWITH%20%7B%0A%20%20SELECT%20%3Fitem%20WHERE%20%7B%0A%20%20%20%20SERVICE%20%3Chttps%3A%2F%2Fquery.wikidata.org%2Fsparql%3E%20%7B%0A%20%20%20%20%20%20%20%20%3Fitem%20wdt%3AP171%2Fwdt%3AP171%2a%20wd%3A'..QID..'.%0A%20%20%20%20%7D%20%0A%20%20%7D%0A%7D%20AS%20%25get_items%0AWHERE%20%7B%0A%20%20INCLUDE%20%25get_items%0A%20%20%3Ffile%20wdt%3AP180%20%3Fitem%20.%0A%20%20%3Ffile%20schema%3AcontentUrl%20%3Furl%20.%0A%20%20BIND%28IRI%28CONCAT%28%22http%3A%2F%2Fcommons.wikimedia.org%2Fwiki%2FSpecial%3AFilePath%2F%22%2C%20wikibase%3AdecodeUri%28SUBSTR%28STR%28%3Furl%29%2C53%29%29%29%29%20AS%20%3Fimage%29%0A%7D '..i18n('taxon-depicted', FALLBACKLANGS)..']'

	return table.concat{
		'<tr class="wdinfo_nomobile">',
			'<td colspan=2 style="text-align: center"><small>',
				'<div class="hlist hlist-separated"><ul>',
					'<li>' .. table.concat(hl, '</li><li>') .. '</li>',
				'</ul></div>',
			'</small></td>',
		'</tr>',
	}
end

local function footer()
	return (config.authoritycontrol or config.helperlinks) and table.concat{
		'<tr><td colspan=2>',
			'<table style="width:100%" id="wdinfo_ac" class="mw-collapsible">',
				authoritycontrol(),
				helperlinks(),
			'</table>',
		'</td></tr>',
	} or ''
end

--- @param eid string: Wikidata entity ID starting with Q or P
local function entityLink( eid )
	local label = getLabel( eid, true )
	local ns = ( eid:sub(1, 1) == 'P' ) and 'Property:' or ''
	return '[[d:'..ns..eid..'|'..label..' <small>('..eid..')</small>]]'
end

--- Generates [[Template:Wikidata Infobox/doc/properties]]
function p.doc()
	local out = {}
	for _, group in ipairs( property_groups ) do
		out[#out+1] = '<h2>' .. group.groupname .. '</h2>'
		if group.comment then
			out[#out+1] = frame:preprocess( group.comment )
		end

		if group.P31_allowed_values then
			local classes = {}
			for _, class in ipairs( group.P31_allowed_values ) do
				classes[#classes+1] = entityLink( class )
			end
			out[#out+1] = 'This group is only shown if the connected Wikidata item is an instance of ' .. table.concat(classes, ' or ') .. '.'
		elseif group.humans_allowed then
			out[#out+1] = 'This group is always shown (by default, a group is ignored if the connected Wikidata item represents a human).'
		end

		local props = {}
		for _, pid in ipairs( group.pids ) do
			props[#props+1] = entityLink( pid )
		end
		out[#out+1] = table.concat( props, ' • ' )
	end

	-- authority control
	out[#out+1] = '<h2>'..getLabel('Q36524')..'</h2>'
	out[#out+1] = 'This group is always shown.'
	for _, group in ipairs( externalIDs ) do
		out[#out+1] = '<h3>' .. group.groupname .. '</h3>'
		local props = {}
		for _, pid in ipairs( group.pids ) do
			props[#props+1] = entityLink( pid )
		end
		out[#out+1] = table.concat( props, ' • ' )
	end

	return table.concat( out, '\n\n' )
end

local function configure( t )
	config.defaultsort      = t['defaultsort']           == 'y'
	config.interwiki        = t['interwiki']             == 'yes'
	config.autocat          = t['autocat']               == 'yes'
	config.trackingcats     = t['trackingcats']          == 'yes'
	config.uploadlink       = t['conf_upload']           == 'yes'
	config.sitelinks        = t['conf_sitelinks']        == 'yes'
	config.authoritycontrol = t['conf_authoritycontrol'] == 'yes'
	config.helperlinks      = t['conf_helperlinks']      == 'yes'

	if t['conf_coordtemplate'] then config.coordtemplate = tonumber( t['conf_coordtemplate'] ) end
	if t['conf_mapwidth'] then config.mapwidth = t['conf_mapwidth'] end
	if t['conf_mapheight'] then config.mapheight = t['conf_mapheight'] end
	if t['conf_imagesize'] then config.imagesize = t['conf_imagesize'] end

	if t['spf'] then config.spf = t['spf'] end
	if t['fwd'] then config.fwd = t['fwd'] end
	if t['osd'] then config.osd = t['osd'] end
	if t['noicon'] then config.noicon = t['noicon'] end
end

function p.main( frame )
	MYLANG = frame:callParserFunction( 'int', 'lang' ) or "en"
	LANG = mw.language.new( MYLANG )
	FALLBACKLANGS = { MYLANG, unpack(mw.language.getFallbacksFor(MYLANG)) }
	QID = frame.args[1]
	ITEM = mw.wikibase.getEntity( QID )
	if not ITEM then
		return '[[Category:Uses of Wikidata Infobox for deleted Wikidata items]]'
	end
	CLAIMS = ITEM.claims
	if not CLAIMS then
		local msg = i18n('noclaims', FALLBACKLANGS):gsub('$1', '[[d:'..QID..'|'..QID..']]' )
		return '[[Category:Uses of Wikidata Infobox with no claims]]<table id="wdinfobox" dir="'..LANG:getDir()..'" class="fileinfotpl-type-information vevent infobox"><tr><td><strong class="error">'..msg..'</strong></td></tr>'
	end

	-- identifying a taxon by checking whether it has a taxon property is faster than checking whether its P31 value is a subclass of taxon
	ISTAXON = CLAIMS['P105'] or CLAIMS['P171'] or CLAIMS['P225'] or CLAIMS['P1843']
	local parentframe = frame:getParent()
	if parentframe then
		configure( parentframe.args )
	end

	for class in iclaims( ITEM:getBestStatements('P31') ) do
		INSTANCEOF[class.id] = true
	end

	local out = {
		metadata(),
		'<table id="wdinfobox" class="fileinfotpl-type-information vevent infobox mw-collapsible" dir="'..LANG:getDir()..'">',
			'<caption class="fn org" id="wdinfoboxcaption">',
				'<b>' .. (ITEM:getLabel() or QID) .. '&nbsp;</b>',
			'</caption>',
			header( config.uploadlink ),
			body(),
			footer(),
			pencil(),
		'</table>',
	}
	if config.trackingcats and os.clock() > 2.5 then -- longer than 2.5 seconds
		out[#out+1] = '[[Category:Uses of Wikidata Infobox with bad performance]]'
	end
	return table.concat( out )
end

function p.debug( qid )
	frame.args = { qid or 'Q42' }
	return p.main( frame )
end

return p

-- Credits:
-- Original authors: Mike Peel with contributions by Jura1
-- 2022 rewrite: LennardHofmann