Module:Coordinates
--[[
 This module is intended to replace the functionality of {{Coord}} and related
 templates.  It provides several methods, including
 
 {{#invoke:Coordinates | coord }} : General function formatting and displaying
 coordinate values.
 
 {{#invoke:Coordinates | dec2dms }} : Simple function for converting decimal
 degree values to DMS format.
 
 {{#invoke:Coordinates | dms2dec }} : Simple function for converting DMS format
 to decimal degree format.
 
 {{#invoke:Coordinates | link }} : Export the link used to reach the tools
 
 ]]
 
 require('strict')
 
 local math_mod = require(&quot;Module:Math&quot;)
 local coordinates = {};
 local isSandbox = mw.getCurrentFrame():getTitle():find('sandbox', 1, true);
 
 local current_page = mw.title.getCurrentTitle()
 local page_name = mw.uri.encode( current_page.prefixedText, 'WIKI' );
 local coord_link = '//geohack.toolforge.org/geohack.php?pagename=' .. page_name .. '&amp;params='
 
 --[[ Helper function, replacement for {{coord/display/title}} ]]
 local function displaytitle(s, notes)
 	local coords = s .. notes;
 
 	return '&lt;!-- COORD INDICATOR START ' .. coords .. ' --&gt;'
 			.. mw.getCurrentFrame():extensionTag{
 				name = 'indicator',
 				args = { name = 'coordinates' },
 				content = '&lt;span id=&quot;coordinates&quot;&gt;[[புவியியல் ஆள்கூற்று முறை|ஆள்கூறுகள்]]: ' .. coords .. '&lt;/span&gt;'
 			}
 			.. '&lt;!-- COORD INDICATOR END --&gt;'
 end
 
 --[[ Helper function, used in detecting DMS formatting ]]
 local function dmsTest(first, second)
 	if type(first) ~= 'string' or type(second) ~= 'string' then
 		return nil
 	end
 	local s = (first .. second):upper()
 	return s:find('^[NS][EW]$') or s:find('^[EW][NS]$')
 end
 
 
 --[[ Wrapper function to grab args, see Module:Arguments for this function's documentation. ]]
 local function makeInvokeFunc(funcName)
 	return function (frame)
 		local args = require('Module:Arguments').getArgs(frame, {
 			wrappers = 'Template:Coord'
 		})
 		return coordinates[funcName](args, frame)
 	end
 end
 
 --[[ Helper function, handle optional args. ]]
 local function optionalArg(arg, supplement)
 	return arg and arg .. supplement or ''
 end
 
 --[[
 Formats any error messages generated for display
 ]]
 local function errorPrinter(errors)
 	local result = &quot;&quot;
 	for i,v in ipairs(errors) do
 		result = result .. '&lt;strong class=&quot;error&quot;&gt;Coordinates: ' .. v[2] .. '&lt;/strong&gt;&lt;br /&gt;'
 	end
 	return result
 end
 
 --[[
 Determine the required CSS class to display coordinates
 
 Usually geo-nondefault is hidden by CSS, unless a user has overridden this for himself
 default is the mode as specificied by the user when calling the {{coord}} template
 mode is the display mode (dec or dms) that we will need to determine the css class for
 ]]
 local function displayDefault(default, mode)
 	if default == &quot;&quot; then
 		default = &quot;dec&quot;
 	end
 
 	if default == mode then
 		return &quot;geo-default&quot;
 	else
 		return &quot;geo-nondefault&quot;
 	end
 end
 
 --[[
 specPrinter
 
 Output formatter.  Takes the structure generated by either parseDec
 or parseDMS and formats it for inclusion on Wikipedia.
 ]]
 local function specPrinter(args, coordinateSpec)
 	local uriComponents = coordinateSpec[&quot;param&quot;]
 	if uriComponents == &quot;&quot; then
 		-- RETURN error, should never be empty or nil
 		return &quot;ERROR param was empty&quot;
 	end
 	if args[&quot;name&quot;] then
 		uriComponents = uriComponents .. &quot;&amp;title=&quot; .. mw.uri.encode(coordinateSpec[&quot;name&quot;])
 	end
 
 	local geodmshtml = '&lt;span class=&quot;geo-dms&quot; title=&quot;Maps, aerial photos, and other data for this location&quot;&gt;'
 			.. '&lt;span class=&quot;latitude&quot;&gt;' .. coordinateSpec[&quot;dms-lat&quot;] .. '&lt;/span&gt; '
 			.. '&lt;span class=&quot;longitude&quot;&gt;' ..coordinateSpec[&quot;dms-long&quot;] .. '&lt;/span&gt;'
 			.. '&lt;/span&gt;'
 
 	local lat = tonumber( coordinateSpec[&quot;dec-lat&quot;] ) or 0
 	local geodeclat
 	if lat &lt; 0 then
 		-- FIXME this breaks the pre-existing precision
 		geodeclat = tostring(coordinateSpec[&quot;dec-lat&quot;]):sub(2) .. &quot;°S&quot;
 	else
 		geodeclat = (coordinateSpec[&quot;dec-lat&quot;] or 0) .. &quot;°N&quot;
 	end
 
 	local long = tonumber( coordinateSpec[&quot;dec-long&quot;] ) or 0
 	local geodeclong
 	if long &lt; 0 then
 		-- FIXME does not handle unicode minus
 		geodeclong = tostring(coordinateSpec[&quot;dec-long&quot;]):sub(2) .. &quot;°W&quot;
 	else
 		geodeclong = (coordinateSpec[&quot;dec-long&quot;] or 0) .. &quot;°E&quot;
 	end
 
 	local geodechtml = '&lt;span class=&quot;geo-dec&quot; title=&quot;Maps, aerial photos, and other data for this location&quot;&gt;'
 			.. geodeclat .. ' '
 			.. geodeclong
 			.. '&lt;/span&gt;'
 
 	local geonumhtml = '&lt;span class=&quot;geo&quot;&gt;'
 			.. coordinateSpec[&quot;dec-lat&quot;] .. '; '
 			.. coordinateSpec[&quot;dec-long&quot;]
 			.. '&lt;/span&gt;'
 
 	local inner = '&lt;span class=&quot;' .. displayDefault(coordinateSpec[&quot;default&quot;], &quot;dms&quot; ) .. '&quot;&gt;' .. geodmshtml .. '&lt;/span&gt;'
 				.. '&lt;span class=&quot;geo-multi-punct&quot;&gt;&amp;#xfeff; / &amp;#xfeff;&lt;/span&gt;'
 				.. '&lt;span class=&quot;' .. displayDefault(coordinateSpec[&quot;default&quot;], &quot;dec&quot; ) .. '&quot;&gt;';
 
 	if not args[&quot;name&quot;] then
 		inner = inner .. geodechtml
 				.. '&lt;span style=&quot;display:none&quot;&gt;&amp;#xfeff; / ' .. geonumhtml .. '&lt;/span&gt;&lt;/span&gt;'
 	else
 		inner = inner .. '&lt;span class=&quot;vcard&quot;&gt;' .. geodechtml
 				.. '&lt;span style=&quot;display:none&quot;&gt;&amp;#xfeff; / ' .. geonumhtml .. '&lt;/span&gt;'
 				.. '&lt;span style=&quot;display:none&quot;&gt;&amp;#xfeff; (&lt;span class=&quot;fn org&quot;&gt;'
 				.. args[&quot;name&quot;] .. '&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;'
 	end
 
     local stylesheetLink = 'Module:Coordinates' .. ( isSandbox and '/sandbox' or '' ) .. '/styles.css'
 	return mw.getCurrentFrame():extensionTag{
 		name = 'templatestyles', args = { src = stylesheetLink }
 	} .. '&lt;span class=&quot;plainlinks nourlexpansion&quot;&gt;[' .. coord_link .. uriComponents ..
 	' ' .. inner .. ']&lt;/span&gt;'
 end
 
 --[[ Helper function, convert decimal to degrees ]]
 local function convert_dec2dms_d(coordinate)
 	local d = math_mod._round( coordinate, 0 ) .. &quot;°&quot;
 	return d .. &quot;&quot;
 end
 
 --[[ Helper function, convert decimal to degrees and minutes ]]
 local function convert_dec2dms_dm(coordinate)
 	coordinate = math_mod._round( coordinate * 60, 0 );
 	local m = coordinate % 60;
 	coordinate = math.floor( (coordinate - m) / 60 );
 	local d = coordinate % 360 ..&quot;°&quot;
 
 	return d .. string.format( &quot;%02d′&quot;, m )
 end
 
 --[[ Helper function, convert decimal to degrees, minutes, and seconds ]]
 local function convert_dec2dms_dms(coordinate)
 	coordinate = math_mod._round( coordinate * 60 * 60, 0 );
 	local s = coordinate % 60
 	coordinate = math.floor( (coordinate - s) / 60 );
 	local m = coordinate % 60
 	coordinate = math.floor( (coordinate - m) / 60 );
 	local d = coordinate % 360 ..&quot;°&quot;
 
 	return d .. string.format( &quot;%02d′&quot;, m ) .. string.format( &quot;%02d″&quot;, s )
 end
 
 --[[
 Helper function, convert decimal latitude or longitude to
 degrees, minutes, and seconds format based on the specified precision.
 ]]
 local function convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
 	local coord = tonumber(coordinate)
 	local postfix
 	if coord &gt;= 0 then
 		postfix = firstPostfix
 	else
 		postfix = secondPostfix
 	end
 
 	precision = precision:lower();
 	if precision == &quot;dms&quot; then
 		return convert_dec2dms_dms( math.abs( coord ) ) .. postfix;
 	elseif precision == &quot;dm&quot; then
 		return convert_dec2dms_dm( math.abs( coord ) ) .. postfix;
 	elseif precision == &quot;d&quot; then
 		return convert_dec2dms_d( math.abs( coord ) ) .. postfix;
 	end
 end
 
 --[[
 Convert DMS format into a N or E decimal coordinate
 ]]
 local function convert_dms2dec(direction, degrees_str, minutes_str, seconds_str)
 	local degrees = tonumber(degrees_str)
 	local minutes = tonumber(minutes_str) or 0
 	local seconds = tonumber(seconds_str) or 0
 
 	local factor = 1
 	if direction == &quot;S&quot; or direction == &quot;W&quot; then
 		factor = -1
 	end
 
 	local precision = 0
 	if seconds_str then
 		precision = 5 + math.max( math_mod._precision(seconds_str), 0 );
 	elseif minutes_str and minutes_str ~= '' then
 		precision = 3 + math.max( math_mod._precision(minutes_str), 0 );
 	else
 		precision = math.max( math_mod._precision(degrees_str), 0 );
 	end
 
 	local decimal = factor * (degrees+(minutes+seconds/60)/60)
 	return string.format( &quot;%.&quot; .. precision .. &quot;f&quot;, decimal ) -- not tonumber since this whole thing is string based.
 end
 
 --[[
 Checks input values to for out of range errors.
 ]]
 local function validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, source, strong )
 	local errors = {};
 	lat_d = tonumber( lat_d ) or 0;
 	lat_m = tonumber( lat_m ) or 0;
 	lat_s = tonumber( lat_s ) or 0;
 	long_d = tonumber( long_d ) or 0;
 	long_m = tonumber( long_m ) or 0;
 	long_s = tonumber( long_s ) or 0;
 
 	if strong then
 		if lat_d &lt; 0 then
 			table.insert(errors, {source, &quot;latitude degrees &lt; 0 with hemisphere flag&quot;})
 		end
 		if long_d &lt; 0 then
 			table.insert(errors, {source, &quot;longitude degrees &lt; 0 with hemisphere flag&quot;})
 		end
 		--[[
 		#coordinates is inconsistent about whether this is an error.  If globe: is
 		specified, it won't error on this condition, but otherwise it will.
 
 		For not simply disable this check.
 
 		if long_d &gt; 180 then
 			table.insert(errors, {source, &quot;longitude degrees &gt; 180 with hemisphere flag&quot;})
 		end
 		]]
 	end
 
 	if lat_d &gt; 90 then
 		table.insert(errors, {source, &quot;latitude degrees &gt; 90&quot;})
 	end
 	if lat_d &lt; -90 then
 		table.insert(errors, {source, &quot;latitude degrees &lt; -90&quot;})
 	end
 	if lat_m &gt;= 60 then
 		table.insert(errors, {source, &quot;latitude minutes &gt;= 60&quot;})
 	end
 	if lat_m &lt; 0 then
 		table.insert(errors, {source, &quot;latitude minutes &lt; 0&quot;})
 	end
 	if lat_s &gt;= 60 then
 		table.insert(errors, {source, &quot;latitude seconds &gt;= 60&quot;})
 	end
 	if lat_s &lt; 0 then
 		table.insert(errors, {source, &quot;latitude seconds &lt; 0&quot;})
 	end
 	if long_d &gt;= 360 then
 		table.insert(errors, {source, &quot;longitude degrees &gt;= 360&quot;})
 	end
 	if long_d &lt;= -360 then
 		table.insert(errors, {source, &quot;longitude degrees &lt;= -360&quot;})
 	end
 	if long_m &gt;= 60 then
 		table.insert(errors, {source, &quot;longitude minutes &gt;= 60&quot;})
 	end
 	if long_m &lt; 0 then
 		table.insert(errors, {source, &quot;longitude minutes &lt; 0&quot;})
 	end
 	if long_s &gt;= 60 then
 		table.insert(errors, {source, &quot;longitude seconds &gt;= 60&quot;})
 	end
 	if long_s &lt; 0 then
 		table.insert(errors, {source, &quot;longitude seconds &lt; 0&quot;})
 	end
 
 	return errors;
 end
 
 --[[
 parseDec
 
 Transforms decimal format latitude and longitude into the
 structure to be used in displaying coordinates
 ]]
 local function parseDec( lat, long, format )
 	local coordinateSpec = {}
 	local errors = {}
 
 	if not long then
 		return nil, {{&quot;parseDec&quot;, &quot;Missing longitude&quot;}}
 	elseif not tonumber(long) then
 		return nil, {{&quot;parseDec&quot;, &quot;Longitude could not be parsed as a number: &quot; .. long}}
 	end
 
 	errors = validate( lat, nil, nil, long, nil, nil, 'parseDec', false );
 	coordinateSpec[&quot;dec-lat&quot;]  = lat;
 	coordinateSpec[&quot;dec-long&quot;] = long;
 
 	local mode = coordinates.determineMode( lat, long );
 	coordinateSpec[&quot;dms-lat&quot;]  = convert_dec2dms( lat, &quot;N&quot;, &quot;S&quot;, mode)  -- {{coord/dec2dms|{{{1}}}|N|S|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
 	coordinateSpec[&quot;dms-long&quot;] = convert_dec2dms( long, &quot;E&quot;, &quot;W&quot;, mode)  -- {{coord/dec2dms|{{{2}}}|E|W|{{coord/prec dec|{{{1}}}|{{{2}}}}}}}
 
 	if format then
 		coordinateSpec.default = format
 	else
 		coordinateSpec.default = &quot;dec&quot;
 	end
 
 	return coordinateSpec, errors
 end
 
 --[[
 parseDMS
 
 Transforms degrees, minutes, seconds format latitude and longitude
 into the a structure to be used in displaying coordinates
 ]]
 local function parseDMS( lat_d, lat_m, lat_s, lat_f, long_d, long_m, long_s, long_f, format )
 	local coordinateSpec, errors, backward = {}, {}
 
 	lat_f = lat_f:upper();
 	long_f = long_f:upper();
 
 	-- Check if specified backward
 	if lat_f == 'E' or lat_f == 'W' then
 		lat_d, long_d, lat_m, long_m, lat_s, long_s, lat_f, long_f, backward = long_d, lat_d, long_m, lat_m, long_s, lat_s, long_f, lat_f, true;
 	end
 
 	errors = validate( lat_d, lat_m, lat_s, long_d, long_m, long_s, 'parseDMS', true );
 	if not long_d then
 		return nil, {{&quot;parseDMS&quot;, &quot;Missing longitude&quot; }}
 	elseif not tonumber(long_d) then
 		return nil, {{&quot;parseDMS&quot;, &quot;Longitude could not be parsed as a number:&quot; .. long_d }}
 	end
 
 	if not lat_m and not lat_s and not long_m and not long_s and #errors == 0 then
 		if math_mod._precision( lat_d ) &gt; 0 or math_mod._precision( long_d ) &gt; 0 then
 			if lat_f:upper() == 'S' then
 				lat_d = '-' .. lat_d;
 			end
 			if long_f:upper() == 'W' then
 				long_d = '-' .. long_d;
 			end
 
 			return parseDec( lat_d, long_d, format );
 		end
 	end
 
 	coordinateSpec[&quot;dms-lat&quot;]  = lat_d..&quot;°&quot;..optionalArg(lat_m,&quot;′&quot;) .. optionalArg(lat_s,&quot;″&quot;) .. lat_f
 	coordinateSpec[&quot;dms-long&quot;] = long_d..&quot;°&quot;..optionalArg(long_m,&quot;′&quot;) .. optionalArg(long_s,&quot;″&quot;) .. long_f
 	coordinateSpec[&quot;dec-lat&quot;]  = convert_dms2dec(lat_f, lat_d, lat_m, lat_s) -- {{coord/dms2dec|{{{4}}}|{{{1}}}|0{{{2}}}|0{{{3}}}}}
 	coordinateSpec[&quot;dec-long&quot;] = convert_dms2dec(long_f, long_d, long_m, long_s) -- {{coord/dms2dec|{{{8}}}|{{{5}}}|0{{{6}}}|0{{{7}}}}}
 
 	if format then
 		coordinateSpec.default = format
 	else
 		coordinateSpec.default = &quot;dms&quot;
 	end
 
 	return coordinateSpec, errors, backward
 end
 
 --[[
 Check the input arguments for coord to determine the kind of data being provided
 and then make the necessary processing.
 ]]
 local function formatTest(args)
 	local result, errors
 	local backward, primary = false, false
 
 	local function getParam(args, lim)
 		local ret = {}
 		for i = 1, lim do
 			ret[i] = args[i] or ''
 		end
 		return table.concat(ret, '_')
 	end
 
 	if not args[1] then
 		-- no lat logic
 		return errorPrinter( {{&quot;formatTest&quot;, &quot;Missing latitude&quot;}} )
 	elseif not tonumber(args[1]) then
 		-- bad lat logic
 		return errorPrinter( {{&quot;formatTest&quot;, &quot;Unable to parse latitude as a number:&quot; .. args[1]}} )
 	elseif not args[4] and not args[5] and not args[6] then
 		-- dec logic
 		result, errors = parseDec(args[1], args[2], args.format)
 		if not result then
 			return errorPrinter(errors);
 		end
 		-- formatting for geohack: geohack expects D_N_D_E notation or D;D notation
 		-- wikiminiatlas doesn't support D;D notation
 		-- #coordinates parserfunction doesn't support negative decimals with NSWE
 		result.param = table.concat({
 			math.abs(tonumber(args[1])),
 			((tonumber(args[1]) or 0) &lt; 0) and 'S' or 'N',
 			math.abs(tonumber(args[2])),
 			((tonumber(args[2]) or 0) &lt; 0) and 'W' or 'E',
 			args[3] or ''}, '_')
 	elseif dmsTest(args[4], args[8]) then
 		-- dms logic
 		result, errors, backward = parseDMS(args[1], args[2], args[3], args[4],
 			args[5], args[6], args[7], args[8], args.format)
 		if args[10] then
 			table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
 		end
 		if not result then
 			return errorPrinter(errors)
 		end
 		result.param = getParam(args, 9)
 	elseif dmsTest(args[3], args[6]) then
 		-- dm logic
 		result, errors, backward = parseDMS(args[1], args[2], nil, args[3],
 			args[4], args[5], nil, args[6], args['format'])
 		if args[8] then
 			table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
 		end
 		if not result then
 			return errorPrinter(errors)
 		end
 		result.param = getParam(args, 7)
 	elseif dmsTest(args[2], args[4]) then
 		-- d logic
 		result, errors, backward = parseDMS(args[1], nil, nil, args[2],
 			args[3], nil, nil, args[4], args.format)
 		if args[6] then
 			table.insert(errors, {'formatTest', 'Extra unexpected parameters'})
 		end
 		if not result then
 			return errorPrinter(errors)
 		end
 		result.param = getParam(args, 5)
 	else
 		-- Error
 		return errorPrinter({{&quot;formatTest&quot;, &quot;Unknown argument format&quot;}}) .. '[[Category:Pages with malformed coordinate tags]]'
 	end
 	result.name = args.name
 
 	local extra_param = {'dim', 'globe', 'scale', 'region', 'source', 'type'}
 	for _, v in ipairs(extra_param) do
 		if args[v] then
 			table.insert(errors, {'formatTest', 'Parameter: &quot;' .. v .. '=&quot; should be &quot;' .. v .. ':&quot;' })
 		end
 	end
 
 	local ret = specPrinter(args, result)
 	if #errors &gt; 0 then
 		ret = ret .. ' ' .. errorPrinter(errors) .. '[[Category:Pages with malformed coordinate tags]]'
 	end
 	return ret, backward
 end
 
 --[[
 Generate Wikidata tracking categories.
 ]]
 local function makeWikidataCategories(qid)
 	local ret
 	local qid = qid or mw.wikibase.getEntityIdForCurrentPage()
 	if mw.wikibase and current_page.namespace == 0 then
 		if qid and mw.wikibase.entityExists(qid) and mw.wikibase.getBestStatements(qid, &quot;P625&quot;) and mw.wikibase.getBestStatements(qid, &quot;P625&quot;)[1] then
 			local snaktype = mw.wikibase.getBestStatements(qid, &quot;P625&quot;)[1].mainsnak.snaktype
 			if snaktype == 'value' then
 				-- coordinates exist both here and on Wikidata, and can be compared.
 				ret = 'Coordinates on Wikidata'
 			elseif snaktype == 'somevalue' then
 				ret = 'Coordinates on Wikidata set to unknown value'
 			elseif snaktype == 'novalue' then
 				ret = 'Coordinates on Wikidata set to no value'
 			end
 		else
 			-- We have to either import the coordinates to Wikidata or remove them here.
 			ret = 'Coordinates not on Wikidata'
 		end
 	end
 	if ret then
 		return string.format('[[Category:%s]]', ret)
 	else
 		return ''
 	end
 end
 
 --[[
 link
 
 Simple function to export the coordinates link for other uses.
 
 Usage:
 	{{#invoke:Coordinates | link }}
 
 ]]
 function coordinates.link(frame)
 	return coord_link;
 end
 
 --[[
 dec2dms
 
 Wrapper to allow templates to call dec2dms directly.
 
 Usage:
 	{{#invoke:Coordinates | dec2dms | decimal_coordinate | positive_suffix |
 		negative_suffix | precision }}
 
 decimal_coordinate is converted to DMS format.  If positive, the positive_suffix
 is appended (typical N or E), if negative, the negative suffix is appended.  The
 specified precision is one of 'D', 'DM', or 'DMS' to specify the level of detail
 to use.
 ]]
 coordinates.dec2dms = makeInvokeFunc('_dec2dms')
 function coordinates._dec2dms(args)
 	local coordinate = args[1]
 	local firstPostfix = args[2] or ''
 	local secondPostfix = args[3] or ''
 	local precision = args[4] or ''
 
 	return convert_dec2dms(coordinate, firstPostfix, secondPostfix, precision)
 end
 
 --[[
 Helper function to determine whether to use D, DM, or DMS
 format depending on the precision of the decimal input.
 ]]
 function coordinates.determineMode( value1, value2 )
 	local precision = math.max( math_mod._precision( value1 ), math_mod._precision( value2 ) );
 	if precision &lt;= 0 then
 		return 'd'
 	elseif precision &lt;= 2 then
 		return 'dm';
 	else
 		return 'dms';
 	end
 end
 
 --[[
 dms2dec
 
 Wrapper to allow templates to call dms2dec directly.
 
 Usage:
 	{{#invoke:Coordinates | dms2dec | direction_flag | degrees |
 		minutes | seconds }}
 
 Converts DMS values specified as degrees, minutes, seconds too decimal format.
 direction_flag is one of N, S, E, W, and determines whether the output is
 positive (i.e. N and E) or negative (i.e. S and W).
 ]]
 coordinates.dms2dec = makeInvokeFunc('_dms2dec')
 function coordinates._dms2dec(args)
 	local direction = args[1]
 	local degrees = args[2]
 	local minutes = args[3]
 	local seconds = args[4]
 
 	return convert_dms2dec(direction, degrees, minutes, seconds)
 end
 
 --[[
 coord
 
 Main entry point for Lua function to replace {{coord}}
 
 Usage:
 	{{#invoke:Coordinates | coord }}
 	{{#invoke:Coordinates | coord | lat | long }}
 	{{#invoke:Coordinates | coord | lat | lat_flag | long | long_flag }}
 	...
 
 	Refer to {{coord}} documentation page for many additional parameters and
 	configuration options.
 
 Note: This function provides the visual display elements of {{coord}}.  In
 order to load coordinates into the database, the {{#coordinates:}} parser
 function must also be called, this is done automatically in the Lua
 version of {{coord}}.
 ]]
 coordinates.coord = makeInvokeFunc('_coord')
 function coordinates._coord(args)
 	if not tonumber(args[1]) and not args[2] then
 		args[3] = args[1]; args[1] = nil
 		local entity = mw.wikibase.getEntityObject(args.qid)
 		if entity
 			and entity.claims
 			and entity.claims.P625
 			and entity.claims.P625[1].mainsnak.snaktype == 'value'
 		then
 			local precision = entity.claims.P625[1].mainsnak.datavalue.value.precision
 			args[1] = entity.claims.P625[1].mainsnak.datavalue.value.latitude
 			args[2] = entity.claims.P625[1].mainsnak.datavalue.value.longitude
 			if precision then
 				precision = -math_mod._round(math.log(precision)/math.log(10),0)
 				args[1] = math_mod._round(args[1],precision)
 				args[2] = math_mod._round(args[2],precision)
 			end
 		end
 	end
 
 	local contents, backward = formatTest(args)
 	local Notes = args.notes or ''
 	local Display = args.display and args.display:lower() or 'inline'
 
 	-- it and ti are short for inline,title and title,inline
 	local function isInline(s)
 		-- Finds whether coordinates are displayed inline.
 		return s:find('inline') ~= nil or s == 'i' or s == 'it' or s == 'ti'
 	end
 	local function isInTitle(s)
 		-- Finds whether coordinates are displayed in the title.
 		return s:find('title') ~= nil or s == 't' or s == 'it' or s == 'ti'
 	end
 
 	local function coord_wrapper(in_args)
 		-- Calls the parser function {{#coordinates:}}.
 		return mw.getCurrentFrame():callParserFunction('#coordinates', in_args) or ''
 	end
 
 	local text = ''
 	if isInline(Display) then
 		text = text .. contents .. Notes
 	end
 	if isInTitle(Display) then
 		text = text .. displaytitle(contents, Notes) .. makeWikidataCategories(args.qid)
 	end
 	if not args.nosave then
 		local page_title, count = mw.title.getCurrentTitle(), 1
 		if backward then
 			local tmp = {}
 			while not string.find((args[count-1] or ''), '[EW]') do tmp[count] = (args[count] or ''); count = count+1 end
 			tmp.count = count; count = 2*(count-1)
 			while count &gt;= tmp.count do table.insert(tmp, 1, (args[count] or '')); count = count-1 end
 			for i, v in ipairs(tmp) do args[i] = v end
 		else
 			while count &lt;= 9 do args[count] = (args[count] or ''); count = count+1 end
 		end
 		if isInTitle(Display) and not page_title.isTalkPage and page_title.subpageText ~= 'doc' and page_title.subpageText ~= 'testcases' then args[10] = 'primary' end
 		args.notes, args.format, args.display = nil
 		text = text .. coord_wrapper(args)
 	end
 	return text
 end
 
 --[[
 coord2text
 
 Extracts a single value from a transclusion of {{Coord}}.
 IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
 
 Usage:
 
     {{#invoke:Coordinates | coord2text | {{Coord}} | parameter }}
 
 Valid values for the second parameter are: lat (signed integer), long (signed integer), type, scale, dim, region, globe, source
 
 ]]
 function coordinates.coord2text(frame)
 	if frame.args[1] == '' or frame.args[2] == '' or not frame.args[2] then return nil end
 	frame.args[2] = mw.text.trim(frame.args[2])
 	if frame.args[2] == 'lat' or frame.args[2] == 'long' then
 		local result, negative = mw.text.split((mw.ustring.match(frame.args[1],'[%.%d]+°[NS] [%.%d]+°[EW]') or ''), ' ')
 		if frame.args[2] == 'lat' then
 			result, negative = result[1], 'S'
 		else
 			result, negative = result[2], 'W'
 		end
 		result = mw.text.split(result, '°')
 		if result[2] == negative then result[1] = '-'..result[1] end
 		return result[1]
 	else
 		return mw.ustring.match(frame.args[1], 'params=.-_'..frame.args[2]..':(.-)[ _]')
 	end
 end
 
 --[[
 coordinsert
 
 Injects some text into the Geohack link of a transclusion of {{Coord}} (if that text isn't already in the transclusion). Outputs the modified transclusion of {{Coord}}.
 IF THE GEOHACK LINK SYNTAX CHANGES THIS FUNCTION MUST BE MODIFIED.
 
 Usage:
 
     {{#invoke:Coordinates | coordinsert | {{Coord}} | parameter:value | parameter:value | … }}
 
 Do not make Geohack unhappy by inserting something which isn't mentioned in the {{Coord}} documentation.
 
 ]]
 function coordinates.coordinsert(frame)
 	-- for the 2nd or later integer parameter (the first is the coord template, as above)
 	for i, v in ipairs(frame.args) do
 		if i ~= 1 then
 			-- if we cannot find in the coord_template the i_th coordinsert parameter e.g. region
 			if not mw.ustring.find(frame.args[1], (mw.ustring.match(frame.args[i], '^(.-:)') or '')) then
 				-- find from the params= up to the first possibly-present underscore
 				-- and append the i_th coordinsert parameter and a space
 				-- IDK why we're adding a space but it does seem somewhat convenient
 				frame.args[1] = mw.ustring.gsub(frame.args[1], '(params=.-)_? ', '%1_'..frame.args[i]..' ')
 			end
 		end
 	end
 	if frame.args.name then
 		-- if we can't find the vcard class
 		if not mw.ustring.find(frame.args[1], '&lt;span class=&quot;vcard&quot;&gt;') then
 			-- take something that looks like a coord template and add the vcard span with class and fn org class
 			local namestr = frame.args.name
 			frame.args[1] = mw.ustring.gsub(
 				frame.args[1],
 				'(&lt;span class=&quot;geo%-default&quot;&gt;)(&lt;span[^&lt;&gt;]*&gt;[^&lt;&gt;]*&lt;/span&gt;&lt;span[^&lt;&gt;]*&gt;[^&lt;&gt;]*&lt;span[^&lt;&gt;]*&gt;[^&lt;&gt;]*&lt;/span&gt;&lt;/span&gt;)(&lt;/span&gt;)',
 				'%1&lt;span class=&quot;vcard&quot;&gt;%2&lt;span style=&quot;display:none&quot;&gt;&amp;#xfeff; (&lt;span class=&quot;fn org&quot;&gt;' .. namestr .. '&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;%3'
 			)
 			-- then find anything from coordinates parameters to the 'end' and attach the title parameter
 			frame.args[1] = mw.ustring.gsub(
 				frame.args[1],
 				'(&amp;params=[^&amp;&quot;&lt;&gt;%[%] ]*) ',
 				'%1&amp;title=' .. mw.uri.encode(namestr) .. ' '
 			)
 		end
 	end
 	
 	-- find the in-source 'indicators' and then call displaytitle
 	mw.ustring.gsub(
 		frame.args[1],
 		'&lt;!%-%- COORD INDICATOR START (.-) %-%-&gt;.-&lt;!%-%- COORD INDICATOR END %-%-&gt;',
 		function (coords) return displaytitle(coords, '') end
 	)
 
 	return frame.args[1]
 end