Jump to content

Wikidata Query Service/Migration/Rewrite of Geospatial Services and Functions

From Wikitech

Geospatial information in WDQS is exposed through both Blazegraph-specific query features and Wikidata’s RDF data model. The Blazegraph extensions have no direct SPARQL 1.1 equivalent, but their behavior can be reproduced using GeoSPARQL together with these structured RDF coordinate properties.

Blazegraph extension functionality

Blazegraph provides two SERVICE extensions - wikibase:around (proximity search) and wikibase:box (bounding-box search) - and a small set of helper functions (geof:globe, geof:latitude, geof:longitude). Separately, Wikidata represents globe-coordinate values as structured RDF: the WKT geometry is available through predicates such as wdt:P625 or ps:P625, while the associated value node reached through psv:P625 exposes wikibase:geoLatitude, wikibase:geoLongitude, wikibase:geoGlobe, and wikibase:geoPrecision.

Blazegraph does not use the correct namespace prefix for "geof:". Blazegraph's prefix is set to "http://www.opengis.net/def/geosparql/function/", whereas the standard is "http://www.opengis.net/def/function/geosparql".

Describing each of the concepts in more detail:

  • wikibase:box finds entities whose coordinate lies within an axis-aligned rectangle defined by two opposite corners
    • The first line of the service request defines the triple pattern used to obtain candidate coordinates, such as ?place wdt:P625 ?location
    • Service parameters define the box using either:
      • The SouthWest + NorthEast corners of the box (wikibase:cornerSoutWest and wikibase:cornerNorthEast)
      • OR the east/west longitudes (wikibase:cornerWest and wikibase:cornerEast)
    • When cornerSouthWest and cornerNorthEast are supplied, the longitude interval is defined directly by the two corner values
      • If the western longitude is numerically greater than the eastern longitude, this form does not automatically imply a box crossing the ±180° meridian
    • When only cornerWest and cornerEast are supplied, the box is determined as follows:
      • Western edge = longitude of cornerWest
      • Eastern edge = longitude of cornerEast
      • Southern edge = minimum latitude of cornerWest, cornerEast
      • Northern edge = maximum latitude of cornerWest, cornerEast
      • If the western longitude is greater than the eastern longitude, the longitude interval crosses the ±180° meridian
      • The southern and northern edges must be calculated explicitly in the rewrite because both are needed to construct the complete bounding region
    • wikibase:globe may be supplied to restrict the search to coordinates on a particular globe
    • If it is omitted, Earth is used by default
  • wikibase:around finds entities whose coordinates lie within a specified radius of a center point
    • The first line of the service request defines a triple pattern of the form, ?place wdt:P625 ?location, referencing an entity's location
      • The location predicate may be any property whose values are globe coordinates - for example, P625 is used to indicate a “coordinate location” for the subject entity
    • Service parameters are wikibase:center, wikibase:radius, wikibase:globe and wikibase:distance (where the globe is the Earth by default and the radius is defined in kilometers)
      • wikibase:distance is an optional OUTPUT parameter whose value is a variable and indicates the computed distance from the radius
      • Note that although the Blazegraph SERVICE takes the globe coordinate system as input, the calculations appear to use an Earth radius (versus the appropriate radius of the astronomical body)
        • Therefore, use of this function on an astronomical body is not reliable
  • geof:globe reads a point's coordinate system from its WKT literal
    • For coordinates originating in Wikidata statements, the same information is also available structurally from the statement value node using wikibase:geoGlobe
  • geof:latitude and geof:longitude define or read latitude and longitude coordinates for a point
    • For coordinates originating in Wikidata statements, these values are also represented explicitly on the statement value node using wikibase:geoLatitude and wikibase:geoLongitude
    • The corresponding value node may additionally provide wikibase:geoPrecision, which records the precision of the coordinate

Note that Blazegraph also implements the GeoSPARQL geof:distance function to calculate the distance between two coordinates on Earth. It uses a units of kilometers (even if an alternate unit is specified). This is translated exactly as-is since QLever also reports distance units in kilometers.[1]

Although this may be obvious, it is worth noting that the Blazegraph helper functions are named geof:globe, geof:latitude, and geof:longitude, while the RDF properties are wikibase:geoGlobe, wikibase:geoLatitude, and wikibase:geoLongitude. These 2 forms are not interchangeable syntactically.

Query Rewrite Patterns

POINT Decomposition

Blazegraph exposes 3 functions to get geometry details from a point - geof:globe, :latitude and :longitude. These can appear anywhere a general SPARQL expression is allowed (SELECT, BIND, FILTER, etc.). They are also emitted internally by this rewriter's wikibase:box handling (which produces geof:latitude()/geof:longitude() calls that are resolved on a later pass). Note that this is a rewrite optimization - the functions cannot be placed inside a wikibase:around/:box service body submitted to Blazegraph (which only accepts its own fixed parameter set).

In Wikidata RDF, globe-coordinate values are represented in two related forms:

  • A geo:wktLiteral, available through predicates such as wdt:P625 or ps:P625[2]
    • Where WKT is an acronym for Well-Known Text
  • A structured value node, available through the corresponding psv: predicate
    • For example:
?item p:P625 ?statement .
?statement ps:P625 ?location ; 
           psv:P625 ?coordinateValue .
?coordinateValue wikibase:geoLatitude ?latitude ;
                 wikibase:geoLongitude ?longitude ;
                 wikibase:geoGlobe ?globe ;
                 wikibase:geoPrecision ?precision .

The object of the truthy predicate, wdt:P625 (or of ps:P625), is the WKT geometry value. For a non-Earth coordinate, the WKT lexical form includes the coordinate-system IRI first, enclosed in angle brackets. For example, for a literal coordinate on Mars:

 <http://www.wikidata.org/entity/Q111> Point(longitude latitude)

For Earth-based coordinates, the coordinate system can be (and usually always is) omitted.

Different from exposing the WKT literal, the psv:P625 value node exposes the individual coordinate components directly as RDF values. When the originating Wikibase statement is available, the properties - wikibase:geoLatitude, wikibase:geoLongitude, wikibase:geoGlobe, and wikibase:geoPrecision - provide the coordinate components as separate RDF values.

For a Wikidata coordinate statement, the structured properties provide a data-model alternative to extracting the components from the WKT lexical form. However, current QLever behavior places significant restrictions on how the two representations can be queried.

At present, QLever does not correctly interpret a WKT literal containing a non-Earth coordinate-system identifier. In addition, applying geometry functions such as geof:minX() and geof:minY() to such a literal leaves the result variables unbound. Therefore, query patterns that bind or operate on a CRS-prefixed literal return no results and do not report an error! This has been reported as an issue[3] on the QLever GitHub site.

Complicating the situation, QLever cannot currently use the structured globe property by itself to discover non-Earth coordinate statements. For example, the following query returns Mars coordinate statements in Blazegraph but no results in QLever:

SELECT ?feature ?coord ?lat ?lon ?globe  WHERE {
   # Find anything with a coordinate location
   ?feature p:P625 ?statement .
   # Step inside the coordinate data node
   ?statement ps:P625 ?coord ; psv:P625 ?coordinate_node .
   ?coordinate_node wikibase:geoLatitude ?lat ; wikibase:geoLongitude ?lon ;
                    wikibase:geoGlobe ?globe .
   # Restrict exclusively to the globe of Mars (Q111)
   FILTER(?globe = wd:Q111)
 } LIMIT 50

It is valuable to note that setting a specific value of the wikibase:geoGlobe predicate in a query does result in a performant (and correct) query result. For example:

SELECT ?feature ?coord ?lat ?lon ?globe WHERE {
   # Get the statement coordinate 
   ?feature p:P625/psv:P625 ?coord .
   # And the coordinate details
   ?coord wikibase:geoLatitude ?lat ; wikibase:geoLongitude ?lon .
   ?coord wikibase:geoGlobe wd:Q111 .   # Set the globe to Mars
   BIND(wd:Q111 AS ?globe)
} LIMIT 50

Returns 50 results.

Given all these caveats, it is important to succintly define the rewrite strategy. The following section describes rewriting the Blazegraph geof:globe, geof:longitude and geof:latitude functions.

geof:globe, geof:longitude and geof:latitude

The three Blazegraph functions - globe, longitude and latitude (namespace http://www.opengis.net/def/geosparql/function/) - extract components of a WKT point. The rewrite strategy is based on one of two algorithms - chosen based on certain criteria explained below. Neither algorithm calls a standard GeoSPARQL geometry function - avoiding problems on a possibly-non-Earth WKT literal.

Preferred: value-node join. If the function's argument traces back to a preceding ?subject wdt:Pxxx ?arg triple, where Pxxx is one of the (currently) ten GlobeCoordinate properties (e.g., P625, P1259, P1332, P1333, P1334, P1335, etc.), the rewrite joins to that statement's value node and reads the structured coordinate property directly. The join is wrapped in an OPTIONAL clause since it could be bound to an "unknown value" (skolemized IRI) rather than a real WKT literal. In this case, it would have no psv: value node. (Blazegraph's own built-ins return that row with the target variable unbound.)

Also, to restrict the evaluation of the OPTIONALs, the join is correlated to **both** the source entity (`?entity p:Pxxx ?stmt`) and the argument itself (`?stmt ps:Pxxx ?arg`). This is required since the argument is a bare coordinate literal, and distinct entities can share the same literal.

To address these scenarios, the rewrite takes the form:

?entity wdt:P625 ?point .
OPTIONAL {
  ?entity p:P625 ?stmt .                       # Repeated
  ?stmt ps:P625 ?point .                       # Join to the statement's value node 
  # Retrieve value node's details via collapsed property path 
  ?stmt psv:P625/wikibase:geoLatitude ?lat .   
  ?stmt psv:P625/wikibase:geoLongitude ?lon .  
  ?stmt psv:P625/wikibase:geoGlobe ?globe .     
}

Note that the output from the rewrite tool will not be as compact as the syntax example above. Why? Each geof:xxx() call is a separate expression occurrence to the rewriter. It has is no notion that `?point` in one BIND is "the same lookup" as `?point` in another BIND three lines down. Each occurrence is processed in isolation - minting fresh variables, building the join, and inserting it. So, the join is not deduplicated across occurrences. It is rebuilt and repeated once per occurrence. In the end, the two forms are equivalent. But, the generated form can be less efficient when multiples of the three functions are called on the same point.

Otherwise: string-parsing. When the argument isn't traceable to a statement (for example, a property-path result, a fixed literal, a BIND-computed value, etc.), the rewrite extracts the coordinate directly from the argument's own lexical form:

# geof:longitude(?x)
xsd:double(STRBEFORE(STRBEFORE(STRAFTER(UCASE(STR(?x)), "POINT("), ")"), " "))

# geof:latitude(?x)
xsd:double(STRAFTER(STRBEFORE(STRAFTER(UCASE(STR(?x)), "POINT("), ")"), " "))

# geof:globe(?x)
IF(STRSTARTS(STR(?x), "<"),
   IRI(STRBEFORE(STRAFTER(STR(?x), "<"), ">")),
   wd:Q2)
Limitations

Blazegraph's native geof:globe() function is unreliable.

Consider the entity, Q90 (Paris). The underlying `wikibase:geoGlobe` triple for its P625 value statement exists and is set to Earth/Q2. However, calling Blazegraph's `geof:globe()` returns nothing for the globe. It appears that Blazegraph simply parses the "Point(long lat)" details in the WKT literal. And, since the globe coordinate system is missing (it is the default Earth), the query returns an empty value.

On the other hand, for completeness and correctness in all rewrites in all locations, rewriting `geof:globe()` for QLever always returns the correct globe value. This is because the globe is determined by the value statement's wikibase:geoGlobe triple or by the literal itself with a fallback to Earth/Q2.

wikibase:around

Given that Blazegraph only uses an Earth radius for calculations, this SERVICE is only valid for Earth-based analyses. Therefore, QLever's lack of support for non-Earth coordinate systems is not an issue.

A rewrite of wikibase:around replaces the service request with the search details (center, radius and distance) as follows:

  • As noted in the Overview, the first line of the :around service is `?place_var ⟨location_predicate⟩ ?location_var`, where the location_predicate is a property IRI which resolves to an object with a geo:wktLiteral data type (e.g., P625)
    • This line is extracted and kept in the rewrite “as-is”
  • bd:serviceParam’s wikibase:center, :radius and :distance names/values are mapped as follows:
    • wikibase:center is either a specific POINT geometry or variable having a wktLiteral data type
      • It is mapped “as defined” into a BIND statement using the geof:distance function (it is geof:distance’s first parameter)
      • The second parameter of geof:distance is the ?location_var from the first line
    • wikibase:radius is either a constant or a variable identifying a numeric value
      • Its units are kilometers
      • Regardless of how radius was written in the original query (an unquoted number, an explicitly ^^xsd:decimal-typed literal, a plain string, or a variable), it is always wrapped in an explicit xsd:decimal(...) cast in the rewrite
    • wikibase:distance is always a variable name that is bound when the service request is evaluated (and used in the output/results)
      • The rewritten form is: BIND(geof:distance(?center_var_name_or_POINT_value, ?location_var) AS ?distance_var_name)
        • Note that although GeoSPARQL's geof:distance() function takes 3 parameters (the POINTs whose distance is to be calculated and the units of measurement), QLever's implementation takes exactly two arguments and always returns kilometers
        • It is important to note that QLever is equivalent to Blazegraph for this calculation
          • Although Blazegraph accepts a units parameter in its distance function, that parameter is ignored (kms are always returned)
    • Plus `FILTER(?distance_var_name <= ?radius_var_name_or_constant)`
  • bd:serviceParam’s wikibase:globe cannot currently be supported by QLever as discussed above
    • Only an Earth coordinate system is valid (for Blazegraph OR QLever)
    • If wikibase:globe is present and set to anything other than Earth (wd:Q2), the rewrite rejects the query as blazegraph_error, code wikibase_around_non_earth_globe
      • Why? There is no non-Earth-valid form that executes on Blazegraph. Although globe can be specified, that parameter is ignored and an Earth-radius used in all cases
      • Instead of silently erroring, the rewrite marks this case as unsupported

Example

The following query returns all airports in a 100km radius around Berlin.

SELECT ?place ?location ?dist 
WHERE {
   wd:Q64 wdt:P625 ?berlinLoc .     # Berlin coordinates
   SERVICE wikibase:around { 
       ?place wdt:P625 ?location . 
       bd:serviceParam wikibase:center ?berlinLoc . 
       bd:serviceParam wikibase:radius "100" . 
       bd:serviceParam wikibase:distance ?dist.
   } 
   FILTER EXISTS {
       # Is an airport
       ?place wdt:P31/wdt:P279* wd:Q1248784 .
   } 
} ORDER BY ASC(?dist)

Which returns the following results: Figure 1

The rewritten query is:

SELECT ?place ?location ?dist 
WHERE {
   wd:Q64 wdt:P625 ?berlinLoc . 
   ?place wdt:P625 ?location .          # Extracted first line
   BIND(geof:distance(?berlinLoc, ?location) AS ?dist)
   FILTER ( ?dist <= xsd:decimal("100") )  
   FILTER EXISTS {
      # Is an airport
      ?place wdt:P31/(wdt:P279)* wd:Q1248784 .
   } 
} ORDER BY ASC(?dist)

Which returns the results: Figure 2

Note that the execution of the rewritten query is very performant, despite the fact that all ?place entities (any entity with a P625 coordinate location) is returned. This happens because QLever employs a spatial index; and its optimiser recognises the implicit test geof:distance(?berlinLoc, ?location) <= 100. Such an index, and such an optimisation, would be required for any query engine to efficiently/performantly run this rewrite.

Also, it is valuable to note that although both queries return 7 results, there is a difference in precision and distance values.

When dealing with geospatial calculations, differences in values may result. For example, if requesting the distance from Berlin to Paris, the following is reported:

  • Blazegraph - 875.9227km
  • QLever - 878.3542km

This is a 2.43 km difference (~0.28%). An issue has been raised on the QLever GitHub site related to this[4].

wikibase:box

wikibase:box specifies a rectangular region using latitude and longitude and ascertains whether a location is within that region. Using SPARQL 1.1 and GeoSPARQL, there are two ways to rewrite the query. Choosing one over the other depends on whether the region includes the meridian (180°0'E or 180°0'W) and whether the user wants to include locations that fall on the meridian.

It is interesting to note that including locations on the meridian result in different answers for QLever and Blazegraph. (See the second example below for details.)

Both Blazegraph and the rewrite strategy handle non-Earth box calculations (despite "non-Earth" coordinates not being supported by QLever). To avoid non-Earth complications, the rewrite avoids use of GeoSPARQL's functions, geof:minX()/minY()/sfContains(), which fail on QLever for non-Earth WKT literals. Instead ...

Rewriting wikibase:box replaces the SERVICE block with:

  • The extracted and copied first line (indicating the ?location variable)
  • Deriving the “bounds” (east/west longitudes and north/south latitudes) from the service details where the various coordinates (the 2 "corners" and the ?location variable) are resolved one of two ways depending on whether:
    • They trace back to a preceding statement (e.g. ?place wdt:P625 ?location)
      • In which case, the rewrite correlates through that statement's value node — ?place p:P625 ?stmt, ?stmt ps:P625 ?location, and ?stmt psv:P625/wikibase:geoLatitude and ?stmt psv:P625/wikibase:geoLongitude for the actual numbers, each inside their own OPTIONAL
        • OPTIONAL is used since the statement may carry an unknown value with no value node at all
    • They are literal WKT constants
      • This could occur for the edges/bounds (as seen in example 2 below)
      • In this case, the rewrite parses the coordinates straight out of the literal's Point(...) text

Note also that a final FILTER comparison is needed. Which form that takes depends on which corner pair was given:

  • The cornerSouthWest/cornerNorthEast parameters are designed to never cross the meridian, so it's a single four-term range check
  • The cornerWest/cornerEast parameters can cross the meridian, so the rewrite adds a wraparound branch and independently determines the box's southerly/northerly extremes via an IF comparison of the two corners' latitudes
    • The wraparound form deliberately includes points that fall exactly on the meridian on either side
      • This is a known, intentional divergence from Blazegraph's asymmetric behavior there, which includes some meridian-boundary points and excludes others (depending on which side of ±180° they fall)

Be aware that neither Blazegraph nor the rewrite verify that the corner terms and the location are on the same globe. Comparing a Mars-tagged box against an Earth location would silently produce a geographically meaningless result rather than an error.

Example 1

The following query checks for schools with the region defined in the service request. It is ordered by the school name for ease of comparison.

SELECT ?place ?placeLabel ?location WHERE {
   wd:Q16553 wdt:P625 ?SJloc.
   wd:Q18013 wdt:P625 ?SCloc.
   SERVICE wikibase:box {
       ?place wdt:P625 ?location.
       bd:serviceParam wikibase:cornerSouthWest ?SJloc.
       bd:serviceParam wikibase:cornerNorthEast ?SCloc.
   }
   FILTER EXISTS { ?place wdt:P31/wdt:P279* wd:Q3914. }    # A school
   SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 
} ORDER BY ?placeLabel

Here are some of the results: Figure 3

The rewritten query is:

SELECT ?place ?placeLabel ?location WHERE {
  wd:Q16553 wdt:P625 ?SJloc .
  wd:Q18013 wdt:P625 ?SCloc .
  ?place wdt:P625 ?location

  OPTIONAL {
    wd:Q16553 <http://www.wikidata.org/prop/P625> ?SJloc__lat_stmt .
    ?SJloc__lat_stmt <http://www.wikidata.org/prop/statement/P625> ?SJloc .
    ?SJloc__lat_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?SJloc__lat
  }
  BIND(?SJloc__lat AS ?s)
  OPTIONAL {
    wd:Q16553 <http://www.wikidata.org/prop/P625> ?SJloc__lon_stmt .
    ?SJloc__lon_stmt <http://www.wikidata.org/prop/statement/P625> ?SJloc .
    ?SJloc__lon_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?SJloc__lon
  }
  BIND(?SJloc__lon AS ?w)
  OPTIONAL {
    wd:Q18013 <http://www.wikidata.org/prop/P625> ?SCloc__lat_stmt .
    ?SCloc__lat_stmt <http://www.wikidata.org/prop/statement/P625> ?SCloc .
    ?SCloc__lat_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?SCloc__lat
  }
  BIND(?SCloc__lat AS ?n)
  OPTIONAL {
    wd:Q18013 <http://www.wikidata.org/prop/P625> ?SCloc__lon_stmt .
    ?SCloc__lon_stmt <http://www.wikidata.org/prop/statement/P625> ?SCloc .
    ?SCloc__lon_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?SCloc__lon
  }
  BIND(?SCloc__lon AS ?e)
  OPTIONAL {
    ?place <http://www.wikidata.org/prop/P625> ?location__lat_stmt .
    ?location__lat_stmt <http://www.wikidata.org/prop/statement/P625> ?location .
    ?location__lat_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?location__lat
  }
  BIND(?location__lat AS ?locLat)
  OPTIONAL {
    ?place <http://www.wikidata.org/prop/P625> ?location__lon_stmt .
    ?location__lon_stmt <http://www.wikidata.org/prop/statement/P625> ?location .
    ?location__lon_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?location__lon
  }
  BIND(?location__lon AS ?locLong)

  FILTER(?locLat >= ?s && ?locLat <= ?n && ?locLong >= ?w && ?locLong <= ?e)

  FILTER EXISTS { ?place wdt:P31/wdt:P279* wd:Q3914 }

  OPTIONAL { ?place rdfs:label ?place__label_value . FILTER(LANG(?place__label_value) = "en") }
  OPTIONAL { ?place rdfs:label ?place__label_value . FILTER(LANG(?place__label_value) = "mul") }
  BIND(IF(BOUND(?place__label_value), STR(?place__label_value), STRAFTER(STR(?place), "entity/")) AS ?placeLabel)
} ORDER BY ?placeLabel

Which returns the same results.

Note that the query above could be collapsed (combining the OPTIONALs together and removing the BINDs) as follows:

SELECT ?place ?placeLabel ?location WHERE {
  wd:Q16553 wdt:P625 ?SJloc .
  wd:Q18013 wdt:P625 ?SCloc .
  ?place wdt:P625 ?location

  OPTIONAL {
    wd:Q16553 <http://www.wikidata.org/prop/P625> ?SJloc_stmt .
    ?SJloc_stmt <http://www.wikidata.org/prop/statement/P625> ?SJloc .
    ?SJloc_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?s ;
                <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?w .
  }
  OPTIONAL {
    wd:Q18013 <http://www.wikidata.org/prop/P625> ?SCloc_stmt .
    ?SCloc_stmt <http://www.wikidata.org/prop/statement/P625> ?SCloc .
    ?SCloc_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?n ;
	            <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?e .
  }
  OPTIONAL {
    ?place <http://www.wikidata.org/prop/P625> ?location_stmt .
    ?location_stmt <http://www.wikidata.org/prop/statement/P625> ?location .
    ?location_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?locLat ;
	               <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?locLong .
  }

  FILTER(?locLat >= ?s && ?locLat <= ?n && ?locLong >= ?w && ?locLong <= ?e)

  FILTER EXISTS { ?place wdt:P31/wdt:P279* wd:Q3914 }

  OPTIONAL { ?place rdfs:label ?place__label_value . FILTER(LANG(?place__label_value) = "en") }
  OPTIONAL { ?place rdfs:label ?place__label_value . FILTER(LANG(?place__label_value) = "mul") }
  BIND(IF(BOUND(?place__label_value), STR(?place__label_value), STRAFTER(STR(?place), "entity/")) AS ?placeLabel)
} ORDER BY ?placeLabel

However, this is not done programmatically. The rewrite generates two fully independent OPTIONALs (for latitude and longitude) per point, each with its own separately-minted statement variable (e.g., ?SJloc__lat_stmt and ?SJloc__lon_stmt). This is an artifact of the rewrite process itself.

In the process of rewriting, an intermediate form of the query is generated that inserts Blazegraph's geof:latitude and geof:longitude functions. This is done since later steps then rewrite those features (and uses relatively complex logic). Each geof:latitude()/geof:longitude() call is resolved on its own with no notion that the other call (only a few lines away) is asking about "the same statement."

Another reason that things are not collapsed is that the rewrite wants to enforce naming-safety. When the rule mints a name such as ?SJloc__lat, it doesn't need to check if that name is in use somewhere else in the query. It is guaranteed "fresh". Then, after the OPTIONAL clause executes, the result is aliased/bound to the final name. Skipping straight to the final name is only safe once a human (or a smarter generator) have already confirmed that the name isn't otherwise in play.

This is left as a future optimization.

Example 2

This next example is a query where only the longitudes are defined because the desired “box” crosses the meridian. Because longitudes wrap at the meridian, any two longitudes can define two paths - the long way (around the globe) and the short way (crossing the meridian).

Using the cornerSouthWest and cornerNorthEast parameters, the result is the “long way”. For example, consider cornerSouthWest=(170,−25) and cornerNorthEast=(−170,−10). The Blazegraph wikibase:box service computes the longitude interval as [min(170,−170), max(170,−170)] = [−170, 170]. This is a 340°-wide band running through longitude 0°. But, when defining cornerWest and cornerEast, the service instead always moves west to east. Since 170 > −170, the service knows to cross ±180° (the correct 20° band over the date line).

The following query searches for all islands in the South Pacific bounded by Fiji, Tonga and Samoa.

SELECT ?place ?loc WHERE {
   SERVICE wikibase:box {
     ?place wdt:P625 ?loc .
     bd:serviceParam wikibase:cornerWest "Point(170 -25)"^^geo:wktLiteral .
     bd:serviceParam wikibase:cornerEast "Point(-170 -10)"^^geo:wktLiteral .
   }
   ?place wdt:P31 wd:Q23442 .          # islands
}

Shown below is a portion of the 891 results.

Figure 4

It is important to note that Blazegraph returns 891 results despite the fact that several islands lie on the meridian. The ones with a 180°0'E longitude are included, but one with a 180°0'W longitude (Rabi Island, Q762070) is excluded.

The closest approximation to Blazegraph's behavior (that is also globe coordinate tolerant) is to define the bounding region directly as a FILTER, comparing latitude and longitude ranges rather than constructing any geometry object. This can be accomplished as follows:

SELECT ?place ?loc WHERE {
  ?place wdt:P625 ?loc

  BIND(xsd:double(STRBEFORE(STRBEFORE(STRAFTER(UCASE(STR("Point(170 -25)"^^geo:wktLiteral)), "POINT("), ")"), " ")) AS ?west)
  BIND(xsd:double(STRBEFORE(STRBEFORE(STRAFTER(UCASE(STR("Point(-170 -10)"^^geo:wktLiteral)), "POINT("), ")"), " ")) AS ?east)
  BIND(xsd:double(STRAFTER(STRBEFORE(STRAFTER(UCASE(STR("Point(170 -25)"^^geo:wktLiteral)), "POINT("), ")"), " ")) AS ?latW)
  BIND(xsd:double(STRAFTER(STRBEFORE(STRAFTER(UCASE(STR("Point(-170 -10)"^^geo:wktLiteral)), "POINT("), ")"), " ")) AS ?latE)
  BIND(IF(?latW < ?latE, ?latW, ?latE) AS ?south)
  BIND(IF(?latW < ?latE, ?latE, ?latW) AS ?north)

  OPTIONAL {
    ?place <http://www.wikidata.org/prop/P625> ?loc__lon_stmt .
    ?loc__lon_stmt <http://www.wikidata.org/prop/statement/P625> ?loc .
    ?loc__lon_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLongitude ?loc__lon
  }
  BIND(?loc__lon AS ?locLong)
  OPTIONAL {
    ?place <http://www.wikidata.org/prop/P625> ?loc__lat_stmt .
    ?loc__lat_stmt <http://www.wikidata.org/prop/statement/P625> ?loc .
    ?loc__lat_stmt <http://www.wikidata.org/prop/statement/value/P625>/wikibase:geoLatitude ?loc__lat
  }
  BIND(?loc__lat AS ?locLat)

  FILTER(?locLat >= ?south && ?locLat <= ?north)
  FILTER ( ( ?west <= ?east && ?locLong >= ?west && ?locLong <= ?east ) ||
           ( ?west >  ?east && (?locLong >= ?west || ?locLong <= ?east) ) )

  ?place wdt:P31 wd:Q23442
}

On QLever, 892 unique results are returned.

Note that the two corner points (170 -25 and -170 -10) are literal WKT constants, not traceable to a preceding statement. They must be resolved via direct string-parsing of the literal (the UCASE/STRBEFORE/STRAFTER chain), rather than the value-node join. However, ?loc (a real bound variable) resolves through wikibase:geoLatitude/geoLongitude via the value-node path, as in Example 1.

GeoSPARQL Compliance

The following list documents the mandatory, minimal GeoSPARQL concepts which are required for Wikidata.

  • POINT and POLYGON geometries
  • Raw geometry literals
    • Written as full geo:wktLiteral coordinate strings, where "full" wktLiterals are defined as specifying the coordinate system, longitude and latitude of a POINT
  • Non-Earth coordinate systems

In addition, the following GeoSPARQL functions are relevant to or used in the Query Rewriter Tool's rules - and so, are also required:

  • geo:asWKT()
  • geof:getSRID()
  • geof:minX(), geof:minY(), geof:maxX(), geof:maxY()
  • geof:distance()
  • geof:sfContains()

QLever GeoSPARQL Compliance

QLever has partial compliance with two GeoSPARQL conformance classes relevant to Wikidata - Geometry and Geometry Topology. The following list describes all 7 of the GeoSPARQL conformance classes, their relevance to Wikidata, and corresponding QLever support:

  • Core Vocabulary (N/A) - The vocabulary defines the classes, geo:SpatialObject and geo:Feature, and their subclass relationships
    • These distinctions are not relevant in the WDQS environment since location is specified by predicates such as wdt:P625 (coordinate location)
  • Topology Vocabulary (N/A) - The vocabulary defines the geo:sf* predicates which can be used in triple data
    • These predicates are not present in Wikidata, but may be returned by federated services such as OpenStreetMap data
    • The predicates are handled by QLever if present in the data
  • Geometry (Partial compliance) - Establishes WKT (Well-Known Text serialization), defines the concepts and properties needed to represent spatial shapes like points, lines, and polygons, and provides GeoSPARQL functions to query geometric attributes such as area, length, distance, and a coordinate reference system
    • QLever supports the following functions: geo:asWKT, geof:distance, :metricDistance, :length, :geometryType, :min/maxX/Y, :envelope, :centroid, :area and :metricArea
    • There is no support for the functions, geof:asGML/asGeoJSON/asKML, :buffer, :metricBuffer, :convexHull, :concaveHull, :boundingCircle, boundary, :union, :intersection, :difference, :symDifference, :perimeter, :metricPerimeter, :getSRID, :relate, :dimension, :coordinateDimension, :spatialDimension, :isEmpty, :isSimple, :is3D, :min/max/Z/M, :isMeasured or :transform
    • At this time, there is no QLever support for non-Earth coordinate systems or for adding a coordinate system to a WKT
  • Geometry DGGS, Discrete Global Grid System (N/A) - An alternate mechanism for representing location (via a grid)
    • Not relevant for WDQS
  • Geometry Topology (Partial compliance) - Provides specific vocabulary and functions to evaluate topological relationships between spatial objects, analyzing how geometries intersect, overlap, touch, or contain one another
    • QLever provides the Simple-Features geof:sf* functions except for geof:sfDisjoint and the functions of the Egenhofer/RCC8 families
    • QLever also provides a geof:sfCovers function, which is not in the GeoSPARQL standard[5]
    • In QLever, sf* functions evaluate as spatial joins where at least one argument must be a multi-valued, spatially-indexed geometry variable (e.g., ?g in ?x wdt:P625 ?g, ranging over the whole P625 column); the other argument may be a constant or query-constructed geometry (e.g. a box polygon)
    • Queries fail when the geometry variable is pinned to one entity (e.g., wd:Q64 wdt:P625 ?g where ?g is a singleton), or when both arguments are constants[6]
  • RDFS Entailment (N/A) - Reasoning and inference
    • N/A for the WDQS environment
  • Query Rewrite (No support) - Automatic translation of queries about geospatial “features” (such as a building or park) into their lower-level geometric calculations
    • Where the Topology Vocabulary defines the relation properties - geo:sfContains, geo:sfWithin, etc. - as RDF predicates, Query Rewrite allows computation of those relations from the default geometries of the “features”
    • Using QLever, testing if two features overlap requires connecting them to their geometries and then testing the geometries with the Geometry Topology functions (such as geof:sfContains)
Caution: Only the wktLiteral serialization is supported by QLever. GML (Geography Markup Language), GeoJSON, KML (Keyhole Markup Language) and DGGS are not supported. And, as of August 2026, globe-prefixed WKT literals silently fail.

Footnotes

  1. Note that if units are specified in Blazegraph, they are ignored. But, if specified in QLever, they result in a parse error.
  2. Other predicates whose values are geo coordinates are P626, P1259, P1332, P1333, P1334, P1335, P2786, P5140 and P9149.
  3. Issue reported as https://github.com/ad-freiburg/qlever/issues/2982
  4. Issue reported as https://github.com/ad-freiburg/qlever/issues/2989
  5. "Covers" exists only in the Egenhofer family, as geof:ehCovers
  6. This is a small non-compliance issue since geof:sf functions should operate over any two geometry literals, but a geof:distance call can be used instead.