Jump to content

Module:CountryData: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(22 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:CountryData
--[[
-- Unified module for country data management.
* Name: CountryData
--
* Author: Mark W. Datysgeld
-- Features:
* Description: Unified module for country data management with JSON loading, normalization, region mapping, and Semantic MediaWiki integration
--  * Loads country data from JSON stored in Data:CountryDataset.json
* Notes: Loads from Data:CountryDataset.json; normalizes country names to canonical forms; maps countries to ICANN regions; provides extensible property access; formats country lists with region-specific emoji styling; processes countries for category assignment
--  * Normalizes country names to canonical forms
]]
--  * Maps countries to ICANN regions
--  * Provides extensible property access
--  * Integrates with Semantic MediaWiki
--  * Formats country lists with region-specific emoji styling
--  * Processes countries for category assignment


-- Dependencies
-- Dependencies
Line 45: Line 40:
end
end


-- Function to safely check if a table has a property
-- Reset the module-level caches (useful for testing)
local function hasProperty(tbl, property)
local function resetCaches()
     return tbl and type(tbl) == "table" and tbl[property] ~= nil
     dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Data Loading Layer
-- Data Loading and Cache Building Layer (Refactored)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


-- Get name lookup cache: builds if not already cached
-- Data loading function using DatasetLoader
local function getNameLookup(data)
local function loadData()
    if dataCache then
        return dataCache
    end
    local raw = loader.get('CountryDataset')
    dataCache = {
        countries      = raw.countries      or {},
        icann_regions  = raw.icann_regions  or {},
        schema_version = raw.schema_version,
        last_updated  = raw.last_updated
    }
    return dataCache
end
 
-- Builds the primary name-to-code lookup cache.
-- This is the core of the refactoring, ensuring correct normalization order.
local function buildNameLookupCache(data)
     if nameLookupCache then
     if nameLookupCache then
         return nameLookupCache
         return nameLookupCache
     end
     end
   
 
    -- If no data provided, return empty lookup
     if not data or not data.countries then
     if not data or not data.countries then
         nameLookupCache = {}
         nameLookupCache = {}
         return nameLookupCache
         return nameLookupCache
     end
     end
   
 
     local lookup = {}
     local lookup = {}
   
    -- Optimization: Pre-count number of mappings
    local mappingCount = 0
     for code, country in pairs(data.countries) do
     for code, country in pairs(data.countries) do
         -- Count canonical name
         -- Ensure the country has a name to process
         mappingCount = mappingCount + 1
         local canonicalName = country.name or country.canonical_name
          
         if canonicalName then
        -- Count variations if they exist
            -- 1. Add the canonical name itself
        if country.variations and type(country.variations) == "table" then
            local normalizedName = NormalizationText.normalizeText(canonicalName)
             mappingCount = mappingCount + #country.variations
            lookup[normalizedName] = code
           
            -- 2. Add the diacritic-stripped version of the canonical name
            local strippedName = DiacriticNormalization.removeDiacritics(canonicalName)
             if strippedName ~= canonicalName then -- only add if it's different
                lookup[NormalizationText.normalizeText(strippedName)] = code
            end
         end
         end
    end
 
   
         -- 3. Process all variations
    -- Build the lookup table with pre-counted size
    for code, country in pairs(data.countries) do
        local names_to_process = {}
       
        -- Add name field as primary display name
        local displayName = country.name or country.canonical_name
        if displayName then
            table.insert(names_to_process, displayName)
        end
       
         -- Add canonical_name if different from name
        if country.canonical_name and country.canonical_name ~= country.name then
            table.insert(names_to_process, country.canonical_name)
        end
       
        -- Add variations
         if country.variations and type(country.variations) == "table" then
         if country.variations and type(country.variations) == "table" then
             for _, variation in pairs(country.variations) do
             for _, variation in pairs(country.variations) do
                 table.insert(names_to_process, variation)
                 -- Add the variation
                local normalizedVariation = NormalizationText.normalizeText(variation)
                lookup[normalizedVariation] = code
               
                -- Add the diacritic-stripped version of the variation
                local strippedVariation = DiacriticNormalization.removeDiacritics(variation)
                if strippedVariation ~= variation then
                    lookup[NormalizationText.normalizeText(strippedVariation)] = code
                end
             end
             end
         end
         end
    end


        for _, name in ipairs(names_to_process) do
            local normalized = NormalizationText.normalizeText(name)
            lookup[normalized] = code
           
            local stripped = DiacriticNormalization.removeDiacritics(normalized)
            if stripped ~= normalized then
                lookup[stripped] = code
            end
        end
    end
   
     nameLookupCache = lookup
     nameLookupCache = lookup
     return lookup
     return lookup
end
end


-- Get region lookup cache: builds if not already cached
-- Builds the region lookup cache.
local function getRegionLookup(data)
local function buildRegionLookupCache(data)
     if regionLookupCache then
     if regionLookupCache then
         return regionLookupCache
         return regionLookupCache
     end
     end
   
 
    -- If no data provided, return empty lookup
     if not data or not data.icann_regions then
     if not data or not data.icann_regions then
         regionLookupCache = {}
         regionLookupCache = {}
         return regionLookupCache
         return regionLookupCache
     end
     end
   
 
     local lookup = {}
     local lookup = {}
   
    -- Optimization: Pre-count number of mappings
    local mappingCount = 0
    for code, region in pairs(data.icann_regions) do
        -- Count canonical name
        mappingCount = mappingCount + 1
       
        -- Count variations if they exist
        if region.variations and type(region.variations) == "table" then
            mappingCount = mappingCount + #region.variations
        end
    end
   
    -- Build the lookup table with pre-counted size
     for code, region in pairs(data.icann_regions) do
     for code, region in pairs(data.icann_regions) do
        -- Add canonical name
         if region.name then
         if region.name then
             lookup[NormalizationText.normalizeText(region.name)] = code
             lookup[NormalizationText.normalizeText(region.name)] = code
         end
         end
       
        -- Add variations
         if region.variations and type(region.variations) == "table" then
         if region.variations and type(region.variations) == "table" then
            -- Use pairs instead of ipairs to handle both array and object structures
             for _, variation in pairs(region.variations) do
             for _, variation in pairs(region.variations) do
                 lookup[NormalizationText.normalizeText(variation)] = code
                 lookup[NormalizationText.normalizeText(variation)] = code
Line 158: Line 138:
         end
         end
     end
     end
   
 
     regionLookupCache = lookup
     regionLookupCache = lookup
     return lookup
     return lookup
end
-- Reset the module-level caches (useful for testing)
local function resetCaches()
    dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end
-- Data loading function using DatasetLoader
local function loadData(frame)
    if dataCache then
        return dataCache
    end
    local raw = loader.get('CountryDataset')
    dataCache = {
        countries      = raw.countries      or {},
        icann_regions  = raw.icann_regions  or {},
        schema_version = raw.schema_version,
        last_updated  = raw.last_updated
    }
    return dataCache
end
end


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Core API Functions
-- Core API Functions (Public Interface)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------


Line 195: Line 151:
-- Load data and initialize caches
-- Load data and initialize caches
function CountryData.loadData(frame)
function CountryData.loadData(frame)
     return loadData(frame)
     return loadData()
end
end


Line 204: Line 160:
end
end


-- Get country data by ISO code
-- Get country data by ISO code (Refactored to use new cache logic)
function CountryData.getCountryByCode(code)
function CountryData.getCountryByCode(code)
     if not code or code == "" then
     if not code or code == "" then
Line 210: Line 166:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryByCode", code)
     local cacheKey = createCacheKey("getCountryByCode", code)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 217: Line 172:
      
      
     local data = loadData()
     local data = loadData()
    code = code:upper() -- Standardize code
      
      
    -- Standardize code to uppercase for consistency
     local result = data and data.countries and data.countries[code] or nil
    code = code:upper()
   
     local result = nil
    if data and data.countries and data.countries[code] then
        result = data.countries[code]
    end
      
      
    -- Cache the result (including nil)
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get country data by name (including variations)
-- Get country data by name (Refactored to use new cache logic)
function CountryData.getCountryByName(name)
function CountryData.getCountryByName(name)
     if not name or name == "" then
     if not name or name == "" then
         return nil
         return nil
     end
     end
   
 
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryByName", name)
     local cacheKey = createCacheKey("getCountryByName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
         return functionCache[cacheKey]
         return functionCache[cacheKey]
     end
     end
   
 
     local data = loadData()
     local data = loadData()
     local nameLookup = getNameLookup(data)
     local nameLookup = buildNameLookupCache(data)
   
 
     -- Normalize the input
     -- Normalize the input name in one go (text normalization includes lowercasing)
     local normalized = NormalizationText.normalizeText(name)
     local normalized = NormalizationText.normalizeText(name)
      
      
     -- Look up the code
     -- First, try a direct lookup with the normalized name
     local code = nameLookup[normalized]
     local code = nameLookup[normalized]
      
 
     -- If not found, try looking up the diacritic-stripped version
    if not code then
        local stripped = DiacriticNormalization.removeDiacritics(name)
        if stripped ~= name then
            code = nameLookup[NormalizationText.normalizeText(stripped)]
        end
    end
 
     local result = nil
     local result = nil
     if code and data.countries[code] then
     if code then
         result = data.countries[code]
         result = data.countries[code]
    else
        -- Try with diacritics removed
        local stripped = DiacriticNormalization.removeDiacritics(normalized)
        if stripped ~= normalized then
            code = nameLookup[stripped]
            if code and data.countries[code] then
                result = data.countries[code]
            end
        end
     end
     end
   
 
    -- Cache the result (including nil)
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
Line 277: Line 223:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountryCodeByName", name)
     local cacheKey = createCacheKey("getCountryCodeByName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 283: Line 228:
     end
     end
      
      
     local data = loadData()
     local country = CountryData.getCountryByName(name)
    local nameLookup = getNameLookup(data)
      
      
     -- Normalize the input
     -- The country object from the dataset doesn't inherently contain its own ISO code key.
    local normalized = NormalizationText.normalizeText(name)
     -- We must iterate through the dataset to find the key corresponding to the found country object.
   
     if country then
     -- Look up the code
         local data = loadData()
    local code = nameLookup[normalized]
         for code, countryData in pairs(data.countries) do
   
            if countryData == country then
     if not code then
                functionCache[cacheKey] = code
        -- Try with diacritics removed
                return code
         local stripped = DiacriticNormalization.removeDiacritics(normalized)
            end
         if stripped ~= normalized then
            code = nameLookup[stripped]
         end
         end
     end
     end
   
 
     -- Cache the result (including nil)
     -- If no country was found, or no matching code was found, cache and return nil.
     functionCache[cacheKey] = code
     functionCache[cacheKey] = nil
     return code
     return nil
end
end
-- (The rest of the functions remain unchanged for now, but will be updated in subsequent phases)


function CountryData.normalizeCountryName(name)
function CountryData.normalizeCountryName(name)
     if not name or name == "" then
     if not name or name == "" then
         return name
         return "(Unrecognized)"
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("normalizeCountryName", name)
     local cacheKey = createCacheKey("normalizeCountryName", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 319: Line 262:
      
      
     local result
     local result
     if country then
     if country and country.name then
        -- Return name as the primary display name
         result = country.name
         result = country.name or country.canonical_name
        -- make it category‑friendly:
        result = result
            :gsub(",%s*", "")                -- drop any commas
            :gsub("%sand the%s+", " and ")    -- turn “ and the ” into “ and ”
     else
     else
        -- If no match, return "(Unrecognized)"
         result = "(Unrecognized)"
         result = "(Unrecognized)"
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get ICANN region for a country
function CountryData.getRegionByCountry(name)
function CountryData.getRegionByCountry(name)
     if not name or name == "" then
     if not name or name == "" then
         return nil
         return "(Unrecognized)"
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getRegionByCountry", name)
     local cacheKey = createCacheKey("getRegionByCountry", name)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 354: Line 288:
         result = country.icann_region
         result = country.icann_region
     else
     else
        -- Return "(Unrecognized)" for consistency with normalizeCountryName
         result = "(Unrecognized)"
         result = "(Unrecognized)"
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get all countries in a specific region
function CountryData.getCountriesByRegion(region)
function CountryData.getCountriesByRegion(region)
     if not region or region == "" then
     if not region or region == "" then
Line 369: Line 300:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getCountriesByRegion", region)
     local cacheKey = createCacheKey("getCountriesByRegion", region)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 376: Line 306:
      
      
     local data = loadData()
     local data = loadData()
     local regionLookup = getRegionLookup(data)
     local regionLookup = buildRegionLookupCache(data)
      
      
    -- Normalize the input
     local normalized = NormalizationText.normalizeText(region)
     local normalized = NormalizationText.normalizeText(region)
   
    -- Look up the region code
     local regionCode = regionLookup[normalized]
     local regionCode = regionLookup[normalized]
      
      
     local result = {}
     local result = {}
     if regionCode and data.countries then
     if regionCode and data.countries then
        -- Pre-count number of countries in region for allocation
        local countryCount = 0
        for _, country in pairs(data.countries) do
            if country.icann_region == regionCode then
                countryCount = countryCount + 1
            end
        end
       
        -- Populate the result with the pre-allocated size
        local index = 1
         for code, country in pairs(data.countries) do
         for code, country in pairs(data.countries) do
             if country.icann_region == regionCode then
             if country.icann_region == regionCode then
                 result[index] = {
                 table.insert(result, {
                     code = code,
                     code = code,
                     name = country.name or country.canonical_name
                     name = country.name
                 }
                 })
                index = index + 1
             end
             end
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get list of all country codes
function CountryData.getAllCountryCodes()
function CountryData.getAllCountryCodes()
    -- Check function cache first
     local cacheKey = "getAllCountryCodes"
     local cacheKey = "getAllCountryCodes"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 422: Line 335:
     local data = loadData()
     local data = loadData()
     local result = {}
     local result = {}
   
     if data and data.countries then
     if data and data.countries then
        -- Pre-allocate the array to the number of countries
        local countryCount = 0
        for _ in pairs(data.countries) do
            countryCount = countryCount + 1
        end
       
        -- Now populate the array
        local index = 1
         for code in pairs(data.countries) do
         for code in pairs(data.countries) do
             result[index] = code
             table.insert(result, code)
            index = index + 1
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get list of all canonical country names
function CountryData.getAllCountryNames()
function CountryData.getAllCountryNames()
    -- Check function cache first
     local cacheKey = "getAllCountryNames"
     local cacheKey = "getAllCountryNames"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 453: Line 353:
     local data = loadData()
     local data = loadData()
     local result = {}
     local result = {}
   
     if data and data.countries then
     if data and data.countries then
        -- Pre-allocate the array to the number of countries
        local countryCount = 0
        for _ in pairs(data.countries) do
            countryCount = countryCount + 1
        end
       
        -- Populate the array
        local index = 1
         for _, country in pairs(data.countries) do
         for _, country in pairs(data.countries) do
             local name = country.name or country.canonical_name
             table.insert(result, country.name)
            result[index] = name
            index = index + 1
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = result
     functionCache[cacheKey] = result
     return result
     return result
end
end


-- Get any property for a country by code
function CountryData.getCountryProperty(code, property)
function CountryData.getCountryProperty(code, property)
     if not code or code == "" or not property or property == "" then
     if not code or code == "" or not property or property == "" then
Line 481: Line 368:
     end
     end
      
      
    -- Check property cache first
     local cacheKey = createCacheKey("getCountryProperty", code, property)
     local cacheKey = createCacheKey("getCountryProperty", code, property)
     if propertyCache[cacheKey] ~= nil then
     if propertyCache[cacheKey] ~= nil then
Line 488: Line 374:
      
      
     local country = CountryData.getCountryByCode(code)
     local country = CountryData.getCountryByCode(code)
    local result = country and country[property] or nil
      
      
    local result = nil
    if country and country[property] ~= nil then
        result = country[property]
    end
   
    -- Cache the result (including nil)
     propertyCache[cacheKey] = result
     propertyCache[cacheKey] = result
     return result
     return result
end
end


-- Get any property for a country by name
function CountryData.getCountryPropertyByName(name, property)
function CountryData.getCountryPropertyByName(name, property)
     if not name or name == "" or not property or property == "" then
     if not name or name == "" or not property or property == "" then
Line 505: Line 385:
     end
     end
      
      
    -- Check property cache first
     local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
     local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
     if propertyCache[cacheKey] ~= nil then
     if propertyCache[cacheKey] ~= nil then
Line 512: Line 391:
      
      
     local code = CountryData.getCountryCodeByName(name)
     local code = CountryData.getCountryCodeByName(name)
   
     local result = nil
     local result = nil
     if code then
     if code then
Line 518: Line 396:
     end
     end
      
      
    -- Cache the result (including nil)
     propertyCache[cacheKey] = result
     propertyCache[cacheKey] = result
     return result
     return result
end
end


-- List all available properties for a country
function CountryData.getAvailableProperties(code)
function CountryData.getAvailableProperties(code)
     if not code or code == "" then
     if not code or code == "" then
Line 529: Line 405:
     end
     end
      
      
    -- Check function cache first
     local cacheKey = createCacheKey("getAvailableProperties", code)
     local cacheKey = createCacheKey("getAvailableProperties", code)
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 535: Line 410:
     end
     end
      
      
     local data = loadData()
     local country = CountryData.getCountryByCode(code)
    if not data or not data.countries or not data.countries[code] then
        return {}
    end
   
     local properties = {}
     local properties = {}
      
     if country then
    -- Pre-allocate the table based on the number of properties
        for property in pairs(country) do
    local propertyCount = 0
            table.insert(properties, property)
    for _ in pairs(data.countries[code]) do
         end
        propertyCount = propertyCount + 1
    end
   
    -- Fill the table with property names
    local index = 1
    for property in pairs(data.countries[code]) do
        properties[index] = property
         index = index + 1
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = properties
     functionCache[cacheKey] = properties
     return properties
     return properties
end
end


-- Get all unique property names across all countries
function CountryData.getAllPropertyNames()
function CountryData.getAllPropertyNames()
    -- Check function cache first
     local cacheKey = "getAllPropertyNames"
     local cacheKey = "getAllPropertyNames"
     if functionCache[cacheKey] ~= nil then
     if functionCache[cacheKey] ~= nil then
Line 569: Line 429:
      
      
     local data = loadData()
     local data = loadData()
    if not data or not data.countries then
        return {}
    end
   
     local properties = {}
     local properties = {}
     local seen = {}
     local seen = {}
      
     if data and data.countries then
    -- First pass: count unique properties for pre-allocation
         for _, country in pairs(data.countries) do
    local propertyCount = 0
            for property in pairs(country) do
    for _, country in pairs(data.countries) do
                if not seen[property] then
        for property in pairs(country) do
                    seen[property] = true
            if not seen[property] then
                    table.insert(properties, property)
                seen[property] = true
                 end
                propertyCount = propertyCount + 1
            end
         end
    end
   
    -- Reset seen table
    seen = {}
   
    -- Second pass: fill the pre-allocated table
    local index = 1
    for _, country in pairs(data.countries) do
        for property in pairs(country) do
            if not seen[property] then
                seen[property] = true
                properties[index] = property
                 index = index + 1
             end
             end
         end
         end
     end
     end
      
      
    -- Cache the result
     functionCache[cacheKey] = properties
     functionCache[cacheKey] = properties
     return properties
     return properties
end
end


 
function CountryData.getSemanticCountryRegionProperties(countryValue)
-- Get semantic properties for countries and regions
-- Returns a table of properties that can be integrated with the batch processing system
function CountryData.getSemanticCountryRegionProperties(countryValue, errorContext)
    -- Debug: Function entry
    local ErrorHandling = require('Module:ErrorHandling')
    local debugContext = ErrorHandling.createContext("CountryData")
    ErrorHandling.addError(debugContext, "FunctionEntryDebug",
        string.format("getSemanticCountryRegionProperties called with: '%s'", countryValue or "nil"),
        "", false)
   
    -- Initialize return table
     local properties = {}
     local properties = {}
   
     if not countryValue or countryValue == "" then
     if not countryValue or countryValue == "" then
        ErrorHandling.addError(debugContext, "EarlyReturnDebug",
            "Returning early: countryValue is nil or empty",
            "", false)
         return properties
         return properties
     end
     end
      
      
     -- Use standard semantic property names directly
     local ConfigRepository = require('Module:ConfigRepository')
     local countryPropertyName = "Has country"
     local countryPropertyName = ConfigRepository.getSemanticPropertyName("Has country")
     local regionPropertyName = "Has ICANN region"
     local regionPropertyName = ConfigRepository.getSemanticPropertyName("Has ICANN region")
   
    if not countryPropertyName or not regionPropertyName then
        return properties
    end
      
      
    -- Split multi-value country strings
     local countries = {}
     local countries = {}
     for country in string.gmatch(countryValue, "[^;]+") do
     for country in string.gmatch(countryValue, "[^;]+") do
Line 641: Line 468:
     end
     end
      
      
    -- Debug: Track country normalization flow
     for _, countryName in ipairs(countries) do
    ErrorHandling.addError(debugContext, "CountryDebug",
         local normalizedCountry = CountryData.normalizeCountryName(countryName)
        string.format("Input='%s' Countries=%d FirstCountry='%s'",
            countryValue or "nil",
            #countries,
            countries[1] or "none"),
        "", false)
   
    -- Process each country
     for _, country in ipairs(countries) do
        -- Debug: Show each country being processed
        ErrorHandling.addError(debugContext, "ProcessingCountryDebug",
            string.format("Processing country: '%s'", country),
            "", false)
       
         local normalizedCountry = CountryData.normalizeCountryName(country)
       
        -- Debug: Show normalization result
        ErrorHandling.addError(debugContext, "NormalizationDebug",
            string.format("'%s' normalized to '%s'", country, normalizedCountry or "nil"),
            "", false)
       
        -- Only process recognized countries
         if normalizedCountry ~= "(Unrecognized)" then
         if normalizedCountry ~= "(Unrecognized)" then
             -- Debug: Show successful recognition
             -- Initialize property tables if they don't exist
            ErrorHandling.addError(debugContext, "RecognizedCountryDebug",
                string.format("Country '%s' recognized as '%s'", country, normalizedCountry),
                "", false)
           
            -- Add country to properties table
             properties[countryPropertyName] = properties[countryPropertyName] or {}
             properties[countryPropertyName] = properties[countryPropertyName] or {}
             table.insert(properties[countryPropertyName], normalizedCountry)
             table.insert(properties[countryPropertyName], normalizedCountry)
              
              
            -- Add region to properties table
             local region = CountryData.getRegionByCountry(normalizedCountry)
             local region = CountryData.getRegionByCountry(country)
             if region and region ~= "(Unrecognized)" then
             if region and region ~= "(Unrecognized)" then
                 properties[regionPropertyName] = properties[regionPropertyName] or {}
                 properties[regionPropertyName] = properties[regionPropertyName] or {}
                 table.insert(properties[regionPropertyName], region)
                 table.insert(properties[regionPropertyName], region)
               
                -- Debug: Show region assignment
                ErrorHandling.addError(debugContext, "RegionDebug",
                    string.format("Country '%s' assigned to region '%s'", normalizedCountry, region),
                    "", false)
             end
             end
        else
            -- Debug: Show unrecognized country
            ErrorHandling.addError(debugContext, "UnrecognizedCountryDebug",
                string.format("Country '%s' not recognized", country),
                "", false)
         end
         end
     end
     end
   
    -- Debug: Show final properties before return
    local propCount = 0
    for _ in pairs(properties) do propCount = propCount + 1 end
    ErrorHandling.addError(debugContext, "FinalPropertiesDebug",
        string.format("Returning %d properties", propCount),
        "", false)
      
      
     return properties
     return properties
end
end


-- Export country data as JSON string (for JavaScript usage)
function CountryData.exportAsJson()
function CountryData.exportAsJson()
     local data = loadData()
     local data = loadData()
   
    -- Ensure we have valid data
     if not data or not data.countries then
     if not data or not data.countries then
         return '{}'
         return '{}'
     end
     end
      
      
    -- Use MediaWiki's JSON encoder
     if mw.text and mw.text.jsonEncode then
     if mw.text and mw.text.jsonEncode then
         local success, result = pcall(function()
         local success, result = pcall(function()
             return mw.text.jsonEncode(data)
             return mw.text.jsonEncode(data)
         end)
         end)
       
         if success and result then
         if success and result then
             return result
             return result
         end
         end
     end
     end
   
    -- Fallback to simple string if JSON encoding fails
     return '{}'
     return '{}'
end
end


--------------------------------------------------------------------------------
-- Country Display Functions with contextual emoji
--------------------------------------------------------------------------------
-- Get region-specific CSS class for country display
local function getRegionClass(region)
local function getRegionClass(region)
     if not region or region == "(Unrecognized)" then
     if not region or region == "(Unrecognized)" then
         return "region-default"
         return "region-default"
     end
     end
   
     if region == "NA" or region == "LAC" then
     if region == "NA" or region == "LAC" then
         return "region-americas"
         return "region-americas"
Line 745: Line 516:
end
end


-- Format a list of countries from a semicolon-separated string
-- Returns either plain text (single country) or bullet points (multiple countries)
-- Each country gets its own region-specific class for styling
function CountryData.formatCountryList(value)
function CountryData.formatCountryList(value)
     if not value or value == "" then return "" end
     if not value or value == "" then return "" end
      
 
     -- Split and normalize countries
     local ListGeneration = require('Module:ListGeneration')
     local countries = {}
    local itemsToProcess = {}
     for country in string.gmatch(value, "[^;]+") do
 
        local trimmed = country:match("^%s*(.-)%s*$")
    -- First, check if the entire string is a single, valid country.
        if trimmed and trimmed ~= "" then
     -- This correctly handles names like "Trinidad and Tobago".
            table.insert(countries, trimmed)
     local singleCountryName = CountryData.normalizeCountryName(value)
    if singleCountryName ~= "(Unrecognized)" then
        -- If it's a valid country, treat it as a single item.
        table.insert(itemsToProcess, value)
     else
        -- If not a single country, assume it's a list and split ONLY by semicolon.
        -- This is safer than letting ListGeneration guess the delimiter.
        for item in string.gmatch(value, "[^;]+") do
            local trimmed = item:match("^%s*(.-)%s*$")
            if trimmed and trimmed ~= "" then
                table.insert(itemsToProcess, trimmed)
            end
         end
         end
     end
     end
   
 
     local normalizedCountries = {}
     -- Define the item hook for country-specific formatting
     local validCountriesCount = 0
     local function countryItemHook(countryName)
   
         local normalized = CountryData.normalizeCountryName(countryName)
    for _, country in ipairs(countries) do
         local normalized = CountryData.normalizeCountryName(country)
        -- Only include recognized countries
         if normalized ~= "(Unrecognized)" then
         if normalized ~= "(Unrecognized)" then
             validCountriesCount = validCountriesCount + 1
             local countryRegion = CountryData.getRegionByCountry(normalized)
             normalizedCountries[validCountriesCount] = normalized
            -- Return a table with content and class for the li element
             return {
                content = normalized,
                class = getRegionClass(countryRegion)
            }
         end
         end
        return nil -- Exclude unrecognized countries from the list
     end
     end
   
 
     -- Generate output based on number of countries
     -- Set the options for the list generation
     if validCountriesCount > 1 then
     local options = {
        local listItems = {}
         mode = 'bullet',
       
         listClass = 'template-list-country',
         for _, country in ipairs(normalizedCountries) do
         itemHook = countryItemHook
            -- Get the region for this specific country
    }
            local countryRegion = CountryData.getRegionByCountry(country)
 
            local regionClass = getRegionClass(countryRegion)
    -- Pass the pre-processed table of items to the list generator.
           
    return ListGeneration.createList(itemsToProcess, options)
            -- Create a list item with region-specific class
            table.insert(listItems, string.format("<li class=\"%s\">%s</li>", regionClass, country))
        end
          
        return string.format("<ul class=\"template-list template-list-country\">%s</ul>",  
                            table.concat(listItems, ""))
    elseif validCountriesCount == 1 then
        -- For a single country, create a similar list with just one item
         local countryRegion = CountryData.getRegionByCountry(normalizedCountries[1])
        local regionClass = getRegionClass(countryRegion)
       
        -- Single item list with the same styling
        return string.format("<ul class=\"template-list template-list-country\"><li class=\"%s\">%s</li></ul>",
                            regionClass, normalizedCountries[1])
    end
   
    return ""
end
end


-- Alias for backward compatibility
function CountryData.formatCountries(value)
function CountryData.formatCountries(value)
     return CountryData.formatCountryList(value)
     return CountryData.formatCountryList(value)
end
end


-- Get a list of normalized countries for category assignment
function CountryData.getCountriesForCategories(value)
function CountryData.getCountriesForCategories(value)
     if not value or value == "" then return {} end
     if not value or value == "" then return {} end
      
      
     local countries = {}
     local countries = {}
     for country in string.gmatch(value, "[^;]+") do
     for countryName in string.gmatch(value, "[^;]+") do
         local trimmed = country:match("^%s*(.-)%s*$")
         local trimmed = countryName:match("^%s*(.-)%s*$")
         if trimmed and trimmed ~= "" then
         if trimmed and trimmed ~= "" then
             table.insert(countries, trimmed)
             local normalized = CountryData.normalizeCountryName(trimmed)
            if normalized ~= "(Unrecognized)" then
                table.insert(countries, normalized)
            end
         end
         end
     end
     end
      
      
     local normalizedCountries = {}
     return countries
    local validCount = 0
   
    for _, country in ipairs(countries) do
        local normalized = CountryData.normalizeCountryName(country)
        -- Only include recognized countries
        if normalized ~= "(Unrecognized)" then
            validCount = validCount + 1
            normalizedCountries[validCount] = normalized
        end
    end
   
    return normalizedCountries
end
end


-- Return the module for use
-- Adds flag filename lookup
function CountryData.getFlagFileName(countryNameOrCode)
function CountryData.getFlagFileName(countryNameOrCode)
     if not countryNameOrCode or countryNameOrCode == '' then return nil end
     if not countryNameOrCode or countryNameOrCode == '' then return nil end
      
      
     local inputName = countryNameOrCode:gsub('_', ' ') -- Clean the input
     local inputName = countryNameOrCode:gsub('_', ' ')
     local isoCode
     local isoCode
      
      
     -- First, try to get the ISO code by treating inputName as a country name.
     -- First, try to get the ISO code by treating inputName as a country name.
    -- CountryData.getCountryCodeByName handles internal normalization.
     isoCode = CountryData.getCountryCodeByName(inputName)  
     isoCode = CountryData.getCountryCodeByName(inputName)  
      
      
Line 847: Line 597:
     -- it might be an ISO code already. Let's validate it.
     -- it might be an ISO code already. Let's validate it.
     if not isoCode and #inputName == 2 then
     if not isoCode and #inputName == 2 then
        -- Check if this 2-char string is a valid country code by attempting to fetch country data.
        -- We use getCountryByCode because it directly uses the code.
         if CountryData.getCountryByCode(inputName) then  
         if CountryData.getCountryByCode(inputName) then  
             isoCode = inputName -- It's a valid code
             isoCode = inputName:upper()
         end
         end
     end
     end
      
      
    -- If we still don't have a valid ISO code, we can't proceed.
     if not isoCode or #isoCode ~= 2 then return nil end
     if not isoCode or isoCode == '' then return nil end
   
    -- Ensure the code is indeed 2 letters long (as a final sanity check).
    if #isoCode ~= 2 then return nil end


    -- Construct the filename in the format "Flag-xx.svg" (e.g., "Flag-ad.svg")
     return 'Flag-' .. string.lower(isoCode) .. '.svg'  
     return 'Flag-' .. string.lower(isoCode) .. '.svg'  
end
end


return CountryData
return CountryData

Latest revision as of 02:57, 25 August 2025

Documentation for this module may be created at Module:CountryData/doc

--[[
* Name: CountryData
* Author: Mark W. Datysgeld
* Description: Unified module for country data management with JSON loading, normalization, region mapping, and Semantic MediaWiki integration
* Notes: Loads from Data:CountryDataset.json; normalizes country names to canonical forms; maps countries to ICANN regions; provides extensible property access; formats country lists with region-specific emoji styling; processes countries for category assignment
]]

-- Dependencies
local DiacriticNormalization = require('Module:NormalizationDiacritic')
local NormalizationText = require('Module:NormalizationText')
local loader = require('Module:DatasetLoader')

-- Module-level cache tables for improved performance
local dataCache = nil
local nameLookupCache = nil
local regionLookupCache = nil
local propertyCache = {}
local functionCache = {}

-- Default data structure to use if JSON loading fails
local DEFAULT_DATA = {
    schema_version = 1,
    last_updated = os.date('!%Y-%m-%dT%H:%M:%SZ'),
    countries = {},
    icann_regions = {}
}

--------------------------------------------------------------------------------
-- Helper Functions
--------------------------------------------------------------------------------

-- Create a cache key from a function name and arguments
local function createCacheKey(funcName, ...)
    local args = {...}
    local keyParts = {funcName}
    for i = 1, #args do
        table.insert(keyParts, tostring(args[i]) or "nil")
    end
    return table.concat(keyParts, ":")
end

-- Reset the module-level caches (useful for testing)
local function resetCaches()
    dataCache = nil
    nameLookupCache = nil
    regionLookupCache = nil
    propertyCache = {}
    functionCache = {}
end

--------------------------------------------------------------------------------
-- Data Loading and Cache Building Layer (Refactored)
--------------------------------------------------------------------------------

-- Data loading function using DatasetLoader
local function loadData()
    if dataCache then
        return dataCache
    end
    local raw = loader.get('CountryDataset')
    dataCache = {
        countries      = raw.countries      or {},
        icann_regions  = raw.icann_regions  or {},
        schema_version = raw.schema_version,
        last_updated   = raw.last_updated
    }
    return dataCache
end

-- Builds the primary name-to-code lookup cache.
-- This is the core of the refactoring, ensuring correct normalization order.
local function buildNameLookupCache(data)
    if nameLookupCache then
        return nameLookupCache
    end

    if not data or not data.countries then
        nameLookupCache = {}
        return nameLookupCache
    end

    local lookup = {}
    for code, country in pairs(data.countries) do
        -- Ensure the country has a name to process
        local canonicalName = country.name or country.canonical_name
        if canonicalName then
            -- 1. Add the canonical name itself
            local normalizedName = NormalizationText.normalizeText(canonicalName)
            lookup[normalizedName] = code
            
            -- 2. Add the diacritic-stripped version of the canonical name
            local strippedName = DiacriticNormalization.removeDiacritics(canonicalName)
            if strippedName ~= canonicalName then -- only add if it's different
                 lookup[NormalizationText.normalizeText(strippedName)] = code
            end
        end

        -- 3. Process all variations
        if country.variations and type(country.variations) == "table" then
            for _, variation in pairs(country.variations) do
                -- Add the variation
                local normalizedVariation = NormalizationText.normalizeText(variation)
                lookup[normalizedVariation] = code
                
                -- Add the diacritic-stripped version of the variation
                local strippedVariation = DiacriticNormalization.removeDiacritics(variation)
                if strippedVariation ~= variation then
                    lookup[NormalizationText.normalizeText(strippedVariation)] = code
                end
            end
        end
    end

    nameLookupCache = lookup
    return lookup
end

-- Builds the region lookup cache.
local function buildRegionLookupCache(data)
    if regionLookupCache then
        return regionLookupCache
    end

    if not data or not data.icann_regions then
        regionLookupCache = {}
        return regionLookupCache
    end

    local lookup = {}
    for code, region in pairs(data.icann_regions) do
        if region.name then
            lookup[NormalizationText.normalizeText(region.name)] = code
        end
        if region.variations and type(region.variations) == "table" then
            for _, variation in pairs(region.variations) do
                lookup[NormalizationText.normalizeText(variation)] = code
            end
        end
    end

    regionLookupCache = lookup
    return lookup
end

--------------------------------------------------------------------------------
-- Core API Functions (Public Interface)
--------------------------------------------------------------------------------

local CountryData = {}

-- Load data and initialize caches
function CountryData.loadData(frame)
    return loadData()
end

-- Reset all caches (primarily for testing)
function CountryData.resetCaches()
    resetCaches()
    return true
end

-- Get country data by ISO code (Refactored to use new cache logic)
function CountryData.getCountryByCode(code)
    if not code or code == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryByCode", code)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    code = code:upper() -- Standardize code
    
    local result = data and data.countries and data.countries[code] or nil
    
    functionCache[cacheKey] = result
    return result
end

-- Get country data by name (Refactored to use new cache logic)
function CountryData.getCountryByName(name)
    if not name or name == "" then
        return nil
    end

    local cacheKey = createCacheKey("getCountryByName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end

    local data = loadData()
    local nameLookup = buildNameLookupCache(data)

    -- Normalize the input name in one go (text normalization includes lowercasing)
    local normalized = NormalizationText.normalizeText(name)
    
    -- First, try a direct lookup with the normalized name
    local code = nameLookup[normalized]

    -- If not found, try looking up the diacritic-stripped version
    if not code then
        local stripped = DiacriticNormalization.removeDiacritics(name)
        if stripped ~= name then
            code = nameLookup[NormalizationText.normalizeText(stripped)]
        end
    end

    local result = nil
    if code then
        result = data.countries[code]
    end

    functionCache[cacheKey] = result
    return result
end

-- Get country code by name
function CountryData.getCountryCodeByName(name)
    if not name or name == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryCodeByName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    -- The country object from the dataset doesn't inherently contain its own ISO code key.
    -- We must iterate through the dataset to find the key corresponding to the found country object.
    if country then
        local data = loadData()
        for code, countryData in pairs(data.countries) do
            if countryData == country then
                functionCache[cacheKey] = code
                return code
            end
        end
    end

    -- If no country was found, or no matching code was found, cache and return nil.
    functionCache[cacheKey] = nil
    return nil
end

-- (The rest of the functions remain unchanged for now, but will be updated in subsequent phases)

function CountryData.normalizeCountryName(name)
    if not name or name == "" then
        return "(Unrecognized)"
    end
    
    local cacheKey = createCacheKey("normalizeCountryName", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    local result
    if country and country.name then
        result = country.name
    else
        result = "(Unrecognized)"
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getRegionByCountry(name)
    if not name or name == "" then
        return "(Unrecognized)"
    end
    
    local cacheKey = createCacheKey("getRegionByCountry", name)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByName(name)
    
    local result
    if country and country.icann_region then
        result = country.icann_region
    else
        result = "(Unrecognized)"
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getCountriesByRegion(region)
    if not region or region == "" then
        return {}
    end
    
    local cacheKey = createCacheKey("getCountriesByRegion", region)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local regionLookup = buildRegionLookupCache(data)
    
    local normalized = NormalizationText.normalizeText(region)
    local regionCode = regionLookup[normalized]
    
    local result = {}
    if regionCode and data.countries then
        for code, country in pairs(data.countries) do
            if country.icann_region == regionCode then
                table.insert(result, {
                    code = code,
                    name = country.name
                })
            end
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getAllCountryCodes()
    local cacheKey = "getAllCountryCodes"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local result = {}
    if data and data.countries then
        for code in pairs(data.countries) do
            table.insert(result, code)
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getAllCountryNames()
    local cacheKey = "getAllCountryNames"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local result = {}
    if data and data.countries then
        for _, country in pairs(data.countries) do
            table.insert(result, country.name)
        end
    end
    
    functionCache[cacheKey] = result
    return result
end

function CountryData.getCountryProperty(code, property)
    if not code or code == "" or not property or property == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryProperty", code, property)
    if propertyCache[cacheKey] ~= nil then
        return propertyCache[cacheKey]
    end
    
    local country = CountryData.getCountryByCode(code)
    local result = country and country[property] or nil
    
    propertyCache[cacheKey] = result
    return result
end

function CountryData.getCountryPropertyByName(name, property)
    if not name or name == "" or not property or property == "" then
        return nil
    end
    
    local cacheKey = createCacheKey("getCountryPropertyByName", name, property)
    if propertyCache[cacheKey] ~= nil then
        return propertyCache[cacheKey]
    end
    
    local code = CountryData.getCountryCodeByName(name)
    local result = nil
    if code then
        result = CountryData.getCountryProperty(code, property)
    end
    
    propertyCache[cacheKey] = result
    return result
end

function CountryData.getAvailableProperties(code)
    if not code or code == "" then
        return {}
    end
    
    local cacheKey = createCacheKey("getAvailableProperties", code)
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local country = CountryData.getCountryByCode(code)
    local properties = {}
    if country then
        for property in pairs(country) do
            table.insert(properties, property)
        end
    end
    
    functionCache[cacheKey] = properties
    return properties
end

function CountryData.getAllPropertyNames()
    local cacheKey = "getAllPropertyNames"
    if functionCache[cacheKey] ~= nil then
        return functionCache[cacheKey]
    end
    
    local data = loadData()
    local properties = {}
    local seen = {}
    if data and data.countries then
        for _, country in pairs(data.countries) do
            for property in pairs(country) do
                if not seen[property] then
                    seen[property] = true
                    table.insert(properties, property)
                end
            end
        end
    end
    
    functionCache[cacheKey] = properties
    return properties
end

function CountryData.getSemanticCountryRegionProperties(countryValue)
    local properties = {}
    if not countryValue or countryValue == "" then
        return properties
    end
    
    local ConfigRepository = require('Module:ConfigRepository')
    local countryPropertyName = ConfigRepository.getSemanticPropertyName("Has country")
    local regionPropertyName = ConfigRepository.getSemanticPropertyName("Has ICANN region")
    
    if not countryPropertyName or not regionPropertyName then
        return properties
    end
    
    local countries = {}
    for country in string.gmatch(countryValue, "[^;]+") do
        local trimmedCountry = country:match("^%s*(.-)%s*$")
        if trimmedCountry and trimmedCountry ~= "" then
            table.insert(countries, trimmedCountry)
        end
    end
    
    for _, countryName in ipairs(countries) do
        local normalizedCountry = CountryData.normalizeCountryName(countryName)
        if normalizedCountry ~= "(Unrecognized)" then
            -- Initialize property tables if they don't exist
            properties[countryPropertyName] = properties[countryPropertyName] or {}
            table.insert(properties[countryPropertyName], normalizedCountry)
            
            local region = CountryData.getRegionByCountry(normalizedCountry)
            if region and region ~= "(Unrecognized)" then
                properties[regionPropertyName] = properties[regionPropertyName] or {}
                table.insert(properties[regionPropertyName], region)
            end
        end
    end
    
    return properties
end

function CountryData.exportAsJson()
    local data = loadData()
    if not data or not data.countries then
        return '{}'
    end
    
    if mw.text and mw.text.jsonEncode then
        local success, result = pcall(function()
            return mw.text.jsonEncode(data)
        end)
        if success and result then
            return result
        end
    end
    return '{}'
end

local function getRegionClass(region)
    if not region or region == "(Unrecognized)" then
        return "region-default"
    end
    if region == "NA" or region == "LAC" then
        return "region-americas"
    elseif region == "AP" then
        return "region-asia-pacific"
    else
        return "region-europe-africa"
    end
end

function CountryData.formatCountryList(value)
    if not value or value == "" then return "" end

    local ListGeneration = require('Module:ListGeneration')
    local itemsToProcess = {}

    -- First, check if the entire string is a single, valid country.
    -- This correctly handles names like "Trinidad and Tobago".
    local singleCountryName = CountryData.normalizeCountryName(value)
    if singleCountryName ~= "(Unrecognized)" then
        -- If it's a valid country, treat it as a single item.
        table.insert(itemsToProcess, value)
    else
        -- If not a single country, assume it's a list and split ONLY by semicolon.
        -- This is safer than letting ListGeneration guess the delimiter.
        for item in string.gmatch(value, "[^;]+") do
            local trimmed = item:match("^%s*(.-)%s*$")
            if trimmed and trimmed ~= "" then
                table.insert(itemsToProcess, trimmed)
            end
        end
    end

    -- Define the item hook for country-specific formatting
    local function countryItemHook(countryName)
        local normalized = CountryData.normalizeCountryName(countryName)
        if normalized ~= "(Unrecognized)" then
            local countryRegion = CountryData.getRegionByCountry(normalized)
            -- Return a table with content and class for the li element
            return {
                content = normalized,
                class = getRegionClass(countryRegion)
            }
        end
        return nil -- Exclude unrecognized countries from the list
    end

    -- Set the options for the list generation
    local options = {
        mode = 'bullet',
        listClass = 'template-list-country',
        itemHook = countryItemHook
    }

    -- Pass the pre-processed table of items to the list generator.
    return ListGeneration.createList(itemsToProcess, options)
end

function CountryData.formatCountries(value)
    return CountryData.formatCountryList(value)
end

function CountryData.getCountriesForCategories(value)
    if not value or value == "" then return {} end
    
    local countries = {}
    for countryName in string.gmatch(value, "[^;]+") do
        local trimmed = countryName:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            local normalized = CountryData.normalizeCountryName(trimmed)
            if normalized ~= "(Unrecognized)" then
                table.insert(countries, normalized)
            end
        end
    end
    
    return countries
end

function CountryData.getFlagFileName(countryNameOrCode)
    if not countryNameOrCode or countryNameOrCode == '' then return nil end
    
    local inputName = countryNameOrCode:gsub('_', ' ')
    local isoCode
    
    -- First, try to get the ISO code by treating inputName as a country name.
    isoCode = CountryData.getCountryCodeByName(inputName) 
    
    -- If no code was found by name, and the inputName itself is 2 characters long,
    -- it might be an ISO code already. Let's validate it.
    if not isoCode and #inputName == 2 then
        if CountryData.getCountryByCode(inputName) then 
            isoCode = inputName:upper()
        end
    end
    
    if not isoCode or #isoCode ~= 2 then return nil end

    return 'Flag-' .. string.lower(isoCode) .. '.svg' 
end

return CountryData