Va ô cuntinutu

Mòdulu:Bozza/GianAntonucci/Wikidata

Dâ Wikipedia, la nciclupidìa lìbbira.

This module provides a comprehensive interface for accessing and formatting Wikidata content in MediaWiki templates. It supports all major Wikidata data types and offers extensive customization options while following performance best practices.

Note: Unless the from parameter is used, the following examples assume they are being run on a page linked to a Wikidata item. For demonstration purposes, many examples use Douglas Adams (Q42) as a reference.

Basic syntax

[cancia lu còdici]
{{#invoke:Bozza/GianAntonucci/Wikidata|function|parameters}}

Quick examples

[cancia lu còdici]
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31}}                    → Human
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P569}}                   → 14 April 1972
{{#invoke:Bozza/GianAntonucci/Wikidata|getQualifier|P39|P580}}              → 20 January 2009

getProperty

[cancia lu còdici]

Retrieves and formats property values from Wikidata.

Parameters:

  • 1 or property – Property ID (required)
  • from – Entity ID to get data from (optional, defaults to current page)
  • rank – Which ranks to include (see rank parameter below)
  • formatting – Output format (see data type formatting below)
  • separator – String to join multiple values (default: , )
  • limit – Maximum number of values to return
  • index – Return only the nth value
  • hasqualifier – Only return statements with this qualifier (see filtering parameters)
  • qualifiervalue – Required value for the qualifier (used with hasqualifier)
  • default – Value to return if no data found

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|formatting=label}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|from=Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P39|hasqualifier=P580}}

getQualifier

[cancia lu còdici]

Retrieves qualifier values from property statements.

Parameters:

  • 1 or property – Property ID (required)
  • 2 or qualifier – Qualifier ID (required)
  • from – Entity ID (optional, defaults to current page)
  • rank – Which ranks to include (see rank parameter)
  • formatting – How to format the output
  • separator – String to join multiple values (default: , )
  • default – Value to return if no data found

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getQualifier|P39|P580}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getQualifier|P39|P580|separator=<br/>}}

Gets the label of a Wikidata entity.

Parameters:

  • 1 or entity – Entity ID (optional, defaults to current page)
  • 2 or lang – Language code (optional, defaults to wiki language)

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getLabel}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getLabel|Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getLabel|Q42|de}}

getDescription

[cancia lu còdici]

Gets the description of a Wikidata entity.

Parameters:

  • 1 or entity – Entity ID (optional, defaults to current page)
  • 2 or lang – Language code (optional, defaults to wiki language)

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getDescription|Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getDescription|Q42|fr}}

Gets both label and description efficiently.

Parameters:

  • 1 or entity – Entity ID (optional, defaults to current page)
  • 2 or lang – Language code (optional)
  • separator – String between label and description (default: - )

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getTerm|Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getTerm|Q42|separator=: }}

Gets the Wikidata entity ID for a page.

Parameters:

  • 1 or page – Page title (optional, defaults to current page)

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|getId}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getId|Douglas Adams}}

Counts the number of statements for a property.

Parameters:

  • 1 or property – Property ID (required)
  • All filtering parameters from getProperty are supported

Examples:

{{#invoke:Bozza/GianAntonucci/Wikidata|count|P50}}
{{#invoke:Bozza/GianAntonucci/Wikidata|count|P50|hasqualifier=P1545}}

Common parameters

[cancia lu còdici]

Formatting parameter

[cancia lu còdici]

The formatting parameter controls how values are displayed:

For entities:

  • raw or id – Returns the entity ID (e.g., Q42)
  • label – Returns only the label
  • sitelink – Returns only the sitelink
  • (default) – Returns a linked label if possible

For other types:

  • raw – Returns unformatted value
  • (default) – Returns formatted value

Rank parameter

[cancia lu còdici]

Controls which statement ranks to include:

  • best – Preferred if available, otherwise normal (default)
  • preferred – Only preferred rank
  • normal – Only normal rank
  • deprecated – Only deprecated rank
  • all – All ranks

Filtering parameters

[cancia lu còdici]
  • hasqualifier – Only include statements with this qualifier
  • qualifiervalue – Required value for the qualifier
  • limit – Maximum number of results
  • index – Select only the nth result

Data type formatting

[cancia lu còdici]

Entities (items and properties)

[cancia lu còdici]
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|from=Q42}} èssiri umanu
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|from=Q42|formatting=label}} èssiri umanu
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|from=Q42|formatting=id}} Q5

Quantities

[cancia lu còdici]
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P2048|from=Q42}} 1,96 metru
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P2048|from=Q42|unitsymbol=true}} 1,96 metru
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P2048|from=Q42|showunit=false}} 1,96 metru

Dates and times

[cancia lu còdici]
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P569|from=Q42}} 11 marzu 1952
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P570|from=Q42}} 11 maju 2001
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P569|from=Q1}}

Different precisions are automatically formatted:

Precision Example output
Day 11 May 2001
Month May 2001
Year 2001
Decade 2000s
Century 21st century
Millennium 3rd millennium

Commons media

[cancia lu còdici]
Code Description
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P18|from=Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P18|from=Q42|size=150px}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P18|from=Q42|thumb|caption=Douglas Adams}}
Douglas Adams
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P856|from=Q42}} douglasadams.com
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P856|from=Q42|formatting=raw}} https://douglasadams.com

Coordinates

[cancia lu còdici]
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P625|from=Q174373|limit=1}} 9.215, 123.514
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P625|from=Q174373|limit=1|coord=latitude}} 9.215, 123.514
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P625|from=Q174373|limit=1|coord=longitude}} 9.215, 123.514

Monolingual text

[cancia lu còdici]
Code Result
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P1477|from=Q42}} Douglas Noël Adams
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P1477|from=Q42|showlang=true}} Douglas Noël Adams (en)

Advanced features

[cancia lu còdici]

Multiple values

[cancia lu còdici]

Control how multiple values are displayed:

{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P50|separator= / }}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P50|limit=3}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P50|index=2}}

Qualifier filtering

[cancia lu còdici]

Filter statements by qualifiers:

{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P39|hasqualifier=P580}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P39|hasqualifier=P580|qualifiervalue=2020}}

Using specific entities

[cancia lu còdici]

Access data from entities other than the current page:

{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P31|from=Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getLabel|Q42}}
{{#invoke:Bozza/GianAntonucci/Wikidata|getDescription|Q42}}

Infobox integration

[cancia lu còdici]
{{Infobox person
| name         = {{#invoke:Bozza/GianAntonucci/Wikidata|getLabel}}
| image        = {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P18|size=250px}}
| birth_date   = {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P569}}
| birth_place  = {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P19|formatting=label}}
| occupation   = {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P106|separator=<br/>}}
}}

Conditional display

[cancia lu còdici]
{{#if: {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P50}} |
'''Authors:''' {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P50|separator=, }}
}}

Complex queries

[cancia lu còdici]
Mayor: {{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P6|hasqualifier=P580}}
Term start: {{#invoke:Bozza/GianAntonucci/Wikidata|getQualifier|P6|P580}}
Term end: {{#invoke:Bozza/GianAntonucci/Wikidata|getQualifier|P6|P582}}

Performance considerations

[cancia lu còdici]
  1. Efficient API usage: The module uses getBestStatements and getAllStatements instead of getEntity, which is much more memory-efficient.
  2. Lazy loading: Data is only fetched when needed, not preloaded.
  3. Caching: MediaWiki caches Wikidata access, so repeated calls to the same property are efficient.
  4. Filtering: The module filters data as early as possible to minimize processing.

Error handling

[cancia lu còdici]

The module provides clear error messages:

  • Property not provided – Shown when the property parameter is missing
  • Qualifier not provided – Shown when the qualifier parameter is missing
  • Entity not found – Shown when the specified entity doesn't exist
  • Unknown data type – Shown for unsupported Wikidata types
  • Unknown entity type – Shown for unrecognised entity types

Errors are wrapped in <span class="error"> tags and can trigger categorization in mainspace.

Suppressing errors

[cancia lu còdici]

Use the default parameter to provide fallback text:

{{#invoke:Bozza/GianAntonucci/Wikidata|getProperty|P999|default=No data available}}

-- Custom Wikidata Module (Fully Fixed Version)
-- Based on best practices from multiple implementations
--
-- This module provides a comprehensive interface to Wikidata for MediaWiki templates.
-- It supports fetching and formatting all major Wikidata data types with extensive
-- customization options.
--
-- FIXED ISSUES:
-- * Correct qualifier filtering (filter BEFORE rank selection)
-- * Eliminated N+1 query problem for unit symbols
-- * Fixed date formatting for large timespans (corrected arithmetic)
-- * Improved year-zero handling
-- * DRY principle applied to repeated code
-- * Better entity type support
--
-- Basic usage:
--   {{#invoke:WikidataModule|getProperty|P31}}
--   {{#invoke:WikidataModule|getProperty|property=P31|formatting=label}}
--   {{#invoke:WikidataModule|getQualifier|P580|P585}}

require('strict')

local p = {}

-- Configuration
local config = {
    -- Error messages
    errors = {
        ['property-not-provided'] = 'Property parameter not provided',
        ['qualifier-not-provided'] = 'Qualifier parameter not provided',
        ['entity-not-found'] = 'Entity not found',
        ['unknown-datatype'] = 'Unknown data type',
        ['unknown-entity-type'] = 'Unknown entity type',
        ['no-claims'] = 'No claims found'
    },
    
    -- Special value labels
    specialValues = {
        somevalue = "''unknown value''",
        novalue = "''no value''"
    },
    
    -- Supported calendar models
    calendars = {
        ['http://www.wikidata.org/entity/Q1985727'] = 'gregorian',
        ['http://www.wikidata.org/entity/Q11184'] = 'julian'
    }
}

-- Helper function for error handling
local function handleError(code)
    local message = config.errors[code] or code
    local namespace = mw.title.getCurrentTitle().namespace
    local category = namespace == 0 and '[[Category:Pages with Wikidata errors]]' or ''
    return string.format('<span class="error">%s</span>%s', message, category)
end

-- Helper function to extract args from frame (DRY principle)
local function getArgs(frame, ...)
    local paramNames = {...}
    local args = {}
    
    -- Check if we have any of the specified parameters in frame.args
    local hasDirectParams = false
    for _, param in ipairs(paramNames) do
        if frame.args[param] then
            hasDirectParams = true
            break
        end
    end
    
    -- Also check for positional parameter
    if frame.args[1] then
        hasDirectParams = true
    end
    
    if hasDirectParams then
        args = frame.args  -- Direct invoke
    else
        args = frame:getParent().args  -- Called from template
    end
    
    return args
end

-- Helper function to generate ordinal suffix (DRY principle)
local function getOrdinalSuffix(n)
    if n % 100 >= 11 and n % 100 <= 13 then
        return 'th'
    elseif n % 10 == 1 then
        return 'st'
    elseif n % 10 == 2 then
        return 'nd'
    elseif n % 10 == 3 then
        return 'rd'
    else
        return 'th'
    end
end

-- Get entity prefix based on entity type (expanded to support more types)
local function getEntityPrefix(entityType)
    local prefixes = {
        item = 'Q',
        property = 'P',
        lexeme = 'L',
        form = 'F',
        sense = 'S'
    }
    return prefixes[entityType]
end

-- Format entity ID (item or property)
local function formatEntityId(entityId, args)
    if args.formatting == 'raw' or args.formatting == 'id' then
        return entityId
    end
    
    local label = mw.wikibase.getLabel(entityId)
    local sitelink = entityId:sub(1,1) == 'Q' and mw.wikibase.getSitelink(entityId) or nil
    
    if args.formatting == 'label' then
        return label or entityId
    elseif args.formatting == 'sitelink' then
        return sitelink or ''
    end
    
    -- Default formatting with link
    if sitelink then
        return label and string.format('[[%s|%s]]', sitelink, label) or string.format('[[%s]]', sitelink)
    elseif label then
        return label
    else
        return entityId
    end
end

-- Format time values with proper precision handling (FIXED)
local function formatTime(timeValue, precision, args)
    local time = timeValue.time
    local calendar = config.calendars[timeValue.calendarmodel] or 'gregorian'
    
    -- Extract year, month, day from ISO format
    local year, month, day = string.match(time, '([+-]?%d+)%-(%d+)%-(%d+)')
    year = tonumber(year)
    month = tonumber(month)
    day = tonumber(day)
    
    if not year then return '' end
    
    -- Handle different precision levels
    if precision <= 5 then  -- Large timespans (FIXED calculation)
        local scales = {
            [0] = {1000000000, "billion years"},
            [1] = {100000000, "hundred million years"},
            [2] = {10000000, "ten million years"},
            [3] = {1000000, "million years"},
            [4] = {100000, "hundred thousand years"},
            [5] = {10000, "ten thousand years"}
        }
        
        local scale = scales[precision]
        if not scale then
            -- Fallback for unexpected precision values
            return tostring(math.abs(year)) .. (year < 0 and ' BCE' or '')
        end
        
        local value = math.abs(year) / scale[1]
        local suffix = year < 0 and ' BCE' or ''
        
        -- Format with appropriate decimal places
        local formatted
        if value >= 10 then
            formatted = string.format('%.0f %s', value, scale[2])
        else
            formatted = string.format('%.1f %s', value, scale[2])
        end
        
        return formatted .. suffix
        
    elseif precision == 6 then  -- Millennium
        local millennium = math.floor((math.abs(year) - 1) / 1000) + 1
        local ordinal = millennium .. getOrdinalSuffix(millennium)
        return ordinal .. ' millennium' .. (year < 0 and ' BCE' or '')
        
    elseif precision == 7 then  -- Century
        local century = math.floor((math.abs(year) - 1) / 100) + 1
        local ordinal = century .. getOrdinalSuffix(century)
        return ordinal .. ' century' .. (year < 0 and ' BCE' or '')
        
    elseif precision == 8 then  -- Decade
        local decade = math.floor(math.abs(year) / 10) * 10
        return decade .. 's' .. (year < 0 and ' BCE' or '')
        
    elseif precision == 9 then  -- Year
        return math.abs(year) .. (year < 0 and ' BCE' or '')
        
    else  -- Month or more precise
        -- Better handling of zero values (FIXED)
        if month == 0 then month = 1 end
        if day == 0 then day = 1 end
        
        -- Reconstruct the time string with fixed values
        time = string.format('%+05d-%02d-%02dT00:00:00Z', year, month, day)
        
        local lang = mw.language.getContentLanguage()
        local formatStr
        
        if precision == 10 then  -- Month
            formatStr = 'F Y'
        elseif precision == 11 then  -- Day
            formatStr = 'j F Y'
        elseif precision >= 12 then  -- Hour or more precise
            formatStr = 'j F Y, H:i' .. (precision >= 14 and ':s' or '')
        end
        
        -- Handle negative years for MediaWiki date formatting
        if year < 0 then
            -- Use positive year for formatting
            time = string.format('%+05d-%02d-%02dT00:00:00Z', -year, month, day)
            local formatted = lang:formatDate(formatStr, time)
            return formatted .. ' BCE'
        else
            return lang:formatDate(formatStr, time)
        end
    end
end

-- Get unit symbol from label (FIXED - no more N+1 queries)
local function getUnitText(unitId, args)
    -- Default to using the label (much more efficient)
    local unitText = mw.wikibase.getLabel(unitId) or ''
    
    -- Only fetch the symbol via P5061 if explicitly requested
    if args.fetchsymbol and unitText ~= '' then
        local statements = mw.wikibase.getBestStatements(unitId, 'P5061')
        if statements and statements[1] and statements[1].mainsnak.snaktype == 'value' then
            -- P5061 returns monolingual text, preferably get 'en' or 'mul' (multilingual)
            for _, statement in ipairs(statements) do
                local lang = statement.mainsnak.datavalue.value.language
                if lang == 'mul' or lang == 'en' then
                    unitText = statement.mainsnak.datavalue.value.text
                    break
                end
            end
            -- If no English or multilingual, use first available
            if unitText == '' and statements[1] then
                unitText = statements[1].mainsnak.datavalue.value.text
            end
        end
    end
    
    return unitText
end

-- Format quantity values
local function formatQuantity(value, args)
    local amount = tonumber(value.amount)
    
    if args.round then
        local mult = 10^(args.round or 0)
        amount = math.floor(amount * mult + 0.5) / mult
    end
    
    if args.formatnum ~= false then
        amount = mw.language.getContentLanguage():formatNum(amount)
    end
    
    -- Handle units if present
    if value.unit and value.unit ~= '1' then
        local unitId = string.match(value.unit, 'Q%d+')
        if unitId and args.showunit ~= false then
            local unitText = getUnitText(unitId, args)
            if unitText ~= '' then
                amount = amount .. ' ' .. unitText
            end
        end
    end
    
    return tostring(amount)
end

-- Format URL values
local function formatUrl(url, args)
    if args.formatting == 'raw' then
        return url
    end
    
    -- Basic URL formatting with line break opportunity
    return string.format('<span class="url">[%s %s]</span>', url, url:gsub('^https?://', ''))
end

-- Format Commons media filenames
local function formatCommonsMedia(filename, args)
    if args.formatting == 'raw' then
        return filename
    end
    
    -- Build image options
    local parts = { 'File:' .. filename }
    
    -- Size/format options
    if args.thumb then
        table.insert(parts, 'thumb')
    elseif args.size then
        table.insert(parts, args.size)
    else
        table.insert(parts, 'frameless')
    end
    
    -- Alignment
    if args.align then
        table.insert(parts, args.align)
    end
    
    -- Alt text
    if args.alt then
        table.insert(parts, 'alt=' .. args.alt)
    end
    
    -- Caption (must be last)
    if args.caption then
        table.insert(parts, args.caption)
    end
    
    return string.format('[[%s]]', table.concat(parts, '|'))
end

-- Main function to format data values
local function formatDatavalue(datavalue, datatype, args)
    if datavalue.type == 'wikibase-entityid' then
        local value = datavalue.value
        local prefix = getEntityPrefix(value['entity-type'])
        if not prefix then
            return handleError('unknown-entity-type')
        end
        local entityId = prefix .. value['numeric-id']
        return formatEntityId(entityId, args)
        
    elseif datavalue.type == 'string' then
        if datatype == 'url' then
            return formatUrl(datavalue.value, args)
        elseif datatype == 'commonsMedia' then
            return formatCommonsMedia(datavalue.value, args)
        else
            -- Regular string, external-id, or math
            return datavalue.value
        end
        
    elseif datavalue.type == 'time' then
        return formatTime(datavalue.value, datavalue.value.precision, args)
        
    elseif datavalue.type == 'quantity' then
        return formatQuantity(datavalue.value, args)
        
    elseif datavalue.type == 'monolingualtext' then
        local text = datavalue.value.text
        local lang = datavalue.value.language
        if args.showlang then
            return string.format('<span lang="%s">%s</span> (%s)', lang, text, lang)
        else
            return text
        end
        
    elseif datavalue.type == 'globecoordinate' then
	    local lat = datavalue.value.latitude
	    local lon = datavalue.value.longitude
	    
	    if args.formatting == 'dms' then
	        -- Convert to DMS format
	        local function toDMS(coord, isLat)
	            local dir = isLat and (coord >= 0 and 'N' or 'S') or (coord >= 0 and 'E' or 'W')
	            coord = math.abs(coord)
	            local deg = math.floor(coord)
	            local min = math.floor((coord - deg) * 60)
	            local sec = ((coord - deg) * 60 - min) * 60
	            return string.format('%d°%d\'%.2f"%s', deg, min, sec, dir)
	        end
	        return toDMS(lat, true) .. ', ' .. toDMS(lon, false)
	    else
	        -- Round decimals
	        lat = math.floor(lat * 1000000 + 0.5) / 1000000
	        lon = math.floor(lon * 1000000 + 0.5) / 1000000
	        return string.format('%s, %s', lat, lon)
	    end
        
    else
        return handleError('unknown-datatype')
    end
end

-- Format a single snak
local function formatSnak(snak, args)
    if snak.snaktype == 'somevalue' then
        return config.specialValues.somevalue
    elseif snak.snaktype == 'novalue' then
        return config.specialValues.novalue
    elseif snak.snaktype == 'value' then
        return formatDatavalue(snak.datavalue, snak.datatype, args)
    end
    return ''
end

-- Get and filter claims (FIXED - proper qualifier filtering)
local function getClaims(propertyId, args)
    local entityId = args.from or mw.wikibase.getEntityIdForCurrentPage()
    if not entityId then
        return nil
    end
    
    -- FIXED: Always get ALL statements first when filtering is needed
    local claims
    if args.hasqualifier or args.qualifiervalue then
        -- Get ALL statements for proper filtering
        claims = mw.wikibase.getAllStatements(entityId, propertyId)
    else
        -- No filtering needed, can use optimized methods
        if args.rank == 'best' or not args.rank then
            claims = mw.wikibase.getBestStatements(entityId, propertyId)
        elseif args.rank == 'all' then
            claims = mw.wikibase.getAllStatements(entityId, propertyId)
        else
            -- Get all and filter by specific rank
            claims = mw.wikibase.getAllStatements(entityId, propertyId)
            local filtered = {}
            for _, claim in ipairs(claims) do
                if claim.rank == args.rank then
                    table.insert(filtered, claim)
                end
            end
            claims = filtered
        end
    end
    
    if not claims or #claims == 0 then
        return nil
    end
    
    -- Filter by qualifier presence if specified
    if args.hasqualifier then
        local filtered = {}
        for _, claim in ipairs(claims) do
            if claim.qualifiers and claim.qualifiers[args.hasqualifier] then
                table.insert(filtered, claim)
            end
        end
        claims = filtered
    end
    
    -- Filter by qualifier value if specified
    if args.qualifiervalue and args.hasqualifier and #claims > 0 then
        local filtered = {}
        for _, claim in ipairs(claims) do
            if claim.qualifiers and claim.qualifiers[args.hasqualifier] then
                for _, qualifier in ipairs(claim.qualifiers[args.hasqualifier]) do
                    local value = formatSnak(qualifier, {formatting = 'raw'})
                    if value == args.qualifiervalue then
                        table.insert(filtered, claim)
                        break
                    end
                end
            end
        end
        claims = filtered
    end
    
    -- NOW apply rank filtering if we had qualifier filtering
    if (args.hasqualifier or args.qualifiervalue) and args.rank and args.rank ~= 'all' and #claims > 0 then
        if args.rank == 'best' then
            -- Manually determine best rank
            local bestRank = 'normal'
            for _, claim in ipairs(claims) do
                if claim.rank == 'preferred' then
                    bestRank = 'preferred'
                    break
                elseif claim.rank == 'normal' then
                    bestRank = 'normal'
                end
            end
            
            -- Filter to only best rank
            local filtered = {}
            for _, claim in ipairs(claims) do
                if claim.rank == bestRank then
                    table.insert(filtered, claim)
                end
            end
            claims = filtered
        else
            -- Filter by specific rank
            local filtered = {}
            for _, claim in ipairs(claims) do
                if claim.rank == args.rank then
                    table.insert(filtered, claim)
                end
            end
            claims = filtered
        end
    end
    
    -- Limit number of results if specified
    if args.limit and #claims > tonumber(args.limit) then
        local limited = {}
        for i = 1, tonumber(args.limit) do
            limited[i] = claims[i]
        end
        claims = limited
    end
    
    -- Select specific index if specified
    if args.index and #claims > 0 then
        local idx = tonumber(args.index)
        if idx and idx > 0 and idx <= #claims then
            claims = { claims[idx] }
        else
            claims = {}
        end
    end
    
    return claims
end

-- Format statements
local function formatStatements(claims, args)
    local results = {}
    
    for _, claim in ipairs(claims) do
        local value = formatSnak(claim.mainsnak, args)
        if value ~= '' then
            table.insert(results, value)
        end
    end
    
    if #results == 0 then
        return nil
    end
    
    -- Join results
    local separator = args.separator or ', '
    return table.concat(results, separator)
end

-------------------------------------------------------------------------------
-- Public functions
-------------------------------------------------------------------------------

---
-- Get property value(s) from Wikidata
function p._getProperty(args)
    local propertyId = args[1] or args.property
    if not propertyId then
        return handleError('property-not-provided')
    end
    
    -- Normalize property ID
    propertyId = string.upper(propertyId)
    
    -- Get claims
    local claims = getClaims(propertyId, args)
    if not claims or #claims == 0 then
        return args.default or ''
    end
    
    -- Format and return
    return formatStatements(claims, args) or args.default or ''
end

-- Template interface
function p.getProperty(frame)
    local args = getArgs(frame, 'property')
    return p._getProperty(args)
end

---
-- Get qualifier value(s) from statements (FIXED - removed redundant check)
function p._getQualifier(args)
    local propertyId = args[1] or args.property
    local qualifierId = args[2] or args.qualifier
    
    if not propertyId then
        return handleError('property-not-provided')
    end
    if not qualifierId then
        return handleError('qualifier-not-provided')
    end
    
    propertyId = string.upper(propertyId)
    qualifierId = string.upper(qualifierId)
    
    -- Get claims already filtered by qualifier presence
    local getClaims_args = {
        from = args.from,
        rank = args.rank or 'best',
        hasqualifier = qualifierId
    }
    
    local claims = getClaims(propertyId, getClaims_args)
    if not claims or #claims == 0 then
        return args.default or ''
    end
    
    -- Extract qualifier values (no redundant check needed)
    local results = {}
    for _, claim in ipairs(claims) do
        -- We know the qualifier exists because getClaims filtered for it
        for _, qualifier in ipairs(claim.qualifiers[qualifierId]) do
            local value = formatSnak(qualifier, args)
            if value ~= '' then
                table.insert(results, value)
            end
        end
    end
    
    if #results == 0 then
        return args.default or ''
    end
    
    local separator = args.separator or ', '
    return table.concat(results, separator)
end

-- Template interface
function p.getQualifier(frame)
    local args = getArgs(frame, 'property', 'qualifier')
    return p._getQualifier(args)
end

---
-- Get Wikidata entity ID for current page or specified title
function p._getId(args)
    local title = args[1] or args.page
    if title then
        local titleObj = mw.title.new(title)
        if titleObj then
            return mw.wikibase.getEntityIdForTitle(titleObj.prefixedText) or ''
        end
    end
    return mw.wikibase.getEntityIdForCurrentPage() or ''
end

-- Template interface
function p.getId(frame)
    local args = getArgs(frame, 'page')
    return p._getId(args)
end

---
-- Get label of a Wikidata entity
function p._getLabel(args)
    local entityId = args[1] or args.entity or args.from
    local lang = args[2] or args.lang
    
    if not entityId then
        entityId = mw.wikibase.getEntityIdForCurrentPage()
    end
    
    if not entityId then
        return ''
    end
    
    entityId = string.upper(entityId)
    
    if lang then
        return mw.wikibase.getLabelByLang(entityId, lang) or ''
    else
        return mw.wikibase.getLabel(entityId) or ''
    end
end

-- Template interface
function p.getLabel(frame)
    local args = getArgs(frame, 'entity', 'from')
    return p._getLabel(args)
end

---
-- Get description of a Wikidata entity
function p._getDescription(args)
    local entityId = args[1] or args.entity or args.from
    local lang = args[2] or args.lang
    
    if not entityId then
        entityId = mw.wikibase.getEntityIdForCurrentPage()
    end
    
    if not entityId then
        return ''
    end
    
    entityId = string.upper(entityId)
    
    if lang then
        local term = mw.wikibase.getTermByLang(entityId, lang)
        return term and term.description or ''
    else
        return mw.wikibase.getDescription(entityId) or ''
    end
end

-- Template interface
function p.getDescription(frame)
    local args = getArgs(frame, 'entity', 'from')
    return p._getDescription(args)
end

---
-- Get both label and description efficiently
function p._getTerm(args)
    local entityId = args[1] or args.entity or args.from
    local lang = args[2] or args.lang
    local separator = args.separator or ' - '
    
    if not entityId then
        entityId = mw.wikibase.getEntityIdForCurrentPage()
    end
    
    if not entityId then
        return ''
    end
    
    entityId = string.upper(entityId)
    
    local label, description
    
    if lang then
        local term = mw.wikibase.getTermByLang(entityId, lang)
        if term then
            label = term.label
            description = term.description
        end
    else
        label = mw.wikibase.getLabel(entityId)
        description = mw.wikibase.getDescription(entityId)
    end
    
    if label and description then
        return label .. separator .. description
    elseif label then
        return label
    elseif description then
        return description
    else
        return ''
    end
end

-- Template interface
function p.getTerm(frame)
    local args = getArgs(frame, 'entity', 'from')
    return p._getTerm(args)
end

---
-- Count number of statements for a property (now works correctly with qualifiers)
function p._count(args)
    local propertyId = args[1] or args.property
    if not propertyId then
        return '0'
    end
    
    propertyId = string.upper(propertyId)
    
    local claims = getClaims(propertyId, args)
    return tostring(claims and #claims or 0)
end

-- Template interface
function p.count(frame)
    local args = getArgs(frame, 'property')
    return p._count(args)
end

return p