Add needs_commons=false option and redesign detail and index pages
Add a needs_commons parameter (default true) to both the API endpoint and the detail page. When needs_commons=false, look up Wikidata items by OSM relation ID (P402) via WDQS to return the most specific matching item even if it has no Wikimedia Commons category. Only activate this path when the matched item has no Commons category, so that locations with a Commons cat always get the same result regardless of the parameter. Remove the nearest-polygon fallback that was returning incorrect results for inland points in broad admin areas (e.g. returning Falmer for a point in Brighton). That fallback found the nearest polygon by boundary distance without requiring containment, so the pin would appear outside the polygon. The geosearch handles these cases correctly. Redesign the detail page: place name as heading, result card, collapsible API response and SPARQL query, improved OSM element cards with left-border highlight on the matched element, and a toggle button between modes. Redesign the index page: two-column layout with numbered steps and API documentation including the needs_commons parameter, Bootstrap form, and examples as a table. Closes #28 (Add support for returning Wikidata item instead of commons category) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
90b009fc90
commit
cd9d8779d3
6 changed files with 385 additions and 138 deletions
|
|
@ -104,6 +104,7 @@ def qid_to_commons_category(qid: str, check_p910: bool = True) -> str | None:
|
|||
|
||||
|
||||
Row = dict[str, dict[str, typing.Any]]
|
||||
Hit = dict[str, str | int | None]
|
||||
|
||||
|
||||
@backoff.on_exception(backoff.expo, QueryError, max_tries=5)
|
||||
|
|
@ -139,10 +140,9 @@ def geosearch_query(lat: float, lon: float) -> str:
|
|||
return query
|
||||
|
||||
|
||||
def geosearch(lat: float, lon: float) -> Row | None:
|
||||
"""Geosearch."""
|
||||
def filter_geosearch_row(rows: list[Row]) -> Row | None:
|
||||
"""Filter geosearch rows for the best match that has a Commons category."""
|
||||
default_max_dist = 1
|
||||
rows = wdqs(geosearch_query(lat, lon))
|
||||
max_dist = {
|
||||
"Q188509": 1, # suburb
|
||||
"Q3957": 2, # town
|
||||
|
|
@ -173,6 +173,33 @@ def geosearch(lat: float, lon: float) -> Row | None:
|
|||
return None
|
||||
|
||||
|
||||
def geosearch(lat: float, lon: float) -> Row | None:
|
||||
"""Geosearch."""
|
||||
rows = wdqs(geosearch_query(lat, lon))
|
||||
return filter_geosearch_row(rows)
|
||||
|
||||
|
||||
def hit_from_row(row: Row) -> Hit:
|
||||
"""Build a Hit from a geosearch row (commons cat is optional)."""
|
||||
qid = wd_to_qid(row["item"])
|
||||
commons_cat: str | None = None
|
||||
if "commonsCat" in row:
|
||||
commons_cat = row["commonsCat"]["value"]
|
||||
elif "commonsSiteLink" in row:
|
||||
site_link = row["commonsSiteLink"]["value"]
|
||||
commons_cat = unescape_title(site_link[len(commons_cat_start):])
|
||||
return {"wikidata": qid, "commons_cat": commons_cat}
|
||||
|
||||
|
||||
def lookup_wikidata_by_osm_relation_ids(relation_ids: list[int]) -> list[Row]:
|
||||
"""Look up Wikidata items that reference the given OSM relation IDs via P402."""
|
||||
query = render_template(
|
||||
"sparql/lookup_by_osm_relation.sparql",
|
||||
relation_ids=[str(r) for r in relation_ids],
|
||||
)
|
||||
return wdqs(query)
|
||||
|
||||
|
||||
def lookup_scottish_parish_in_wikidata(code: str) -> list[Row]:
|
||||
"""Lookup scottish parish in Wikidata."""
|
||||
return wdqs(render_template("sparql/scottish_parish.sparql", code=code))
|
||||
|
|
@ -196,9 +223,6 @@ def unescape_title(t: str) -> str:
|
|||
return urllib.parse.unquote(t.replace("_", " "))
|
||||
|
||||
|
||||
Hit = dict[str, str | int | None]
|
||||
|
||||
|
||||
def commons_from_rows(rows: list[Row]) -> Hit | None:
|
||||
"""Commons from rows."""
|
||||
for row in rows:
|
||||
|
|
|
|||
77
lookup.py
77
lookup.py
|
|
@ -106,7 +106,9 @@ def add_missing_commons_cat(rows: list[StrDict]) -> None:
|
|||
row["commonsCat"] = {"type": "literal", "value": commons_cat}
|
||||
|
||||
|
||||
def lat_lon_to_wikidata(lat: float, lon: float) -> dict[str, typing.Any]:
|
||||
def lat_lon_to_wikidata(
|
||||
lat: float, lon: float, needs_commons: bool = True
|
||||
) -> dict[str, typing.Any]:
|
||||
"""Lookup lat/lon and find most appropriate Wikidata item."""
|
||||
scotland_code = scotland.get_scotland_code(lat, lon)
|
||||
|
||||
|
|
@ -148,32 +150,54 @@ def lat_lon_to_wikidata(lat: float, lon: float) -> dict[str, typing.Any]:
|
|||
if not nearby_result.get("missing"):
|
||||
return {"elements": elements, "result": nearby_result}
|
||||
|
||||
# Point is in a broad area (e.g. country) — try nearest specific polygon
|
||||
nearby = model.Polygon.nearest(lat, lon)
|
||||
if nearby and nearby.tags:
|
||||
tags: typing.Mapping[str, str] = nearby.tags
|
||||
al = get_admin_level(tags)
|
||||
hit = (
|
||||
hit_from_wikidata_tag(tags)
|
||||
or hit_from_ref_gss_tag(tags)
|
||||
or hit_from_name(tags, lat, lon)
|
||||
)
|
||||
if hit:
|
||||
hit["admin_level"] = al
|
||||
hit["element"] = nearby.osm_id
|
||||
hit["geojson"] = typing.cast(str, nearby.geojson_str)
|
||||
nearby_result = wikidata.build_dict(hit, lat, lon)
|
||||
if not nearby_result.get("missing"):
|
||||
return {"elements": elements, "result": nearby_result}
|
||||
if not needs_commons:
|
||||
# Direct lookup: find Wikidata items whose P402 (OSM relation ID) matches
|
||||
# one of the OSM polygons that contain this point.
|
||||
osm_id_to_element: dict[int, model.Polygon] = {}
|
||||
relation_ids_for_lookup: list[int] = []
|
||||
for e in elements:
|
||||
if e.osm_id < 0:
|
||||
rel_id = abs(e.osm_id)
|
||||
relation_ids_for_lookup.append(rel_id)
|
||||
osm_id_to_element[e.osm_id] = e
|
||||
|
||||
row = wikidata.geosearch(lat, lon)
|
||||
if relation_ids_for_lookup:
|
||||
lookup_rows = wikidata.lookup_wikidata_by_osm_relation_ids(
|
||||
relation_ids_for_lookup
|
||||
)
|
||||
rel_to_hit: dict[int, wikidata.Hit] = {}
|
||||
for row in lookup_rows:
|
||||
rel_id = int(row["osmRelation"]["value"])
|
||||
if rel_id not in rel_to_hit:
|
||||
rel_to_hit[rel_id] = wikidata.hit_from_row(row)
|
||||
|
||||
# Iterate elements in specificity order (smallest area first, from coords_within).
|
||||
# Only use hits without a Commons category — if there's a Commons cat,
|
||||
# the geosearch path will find it (or something more specific).
|
||||
for e in elements:
|
||||
if e.osm_id >= 0:
|
||||
continue
|
||||
rel_id = abs(e.osm_id)
|
||||
if rel_id not in rel_to_hit:
|
||||
continue
|
||||
hit = rel_to_hit[rel_id]
|
||||
if hit.get("commons_cat"):
|
||||
continue
|
||||
if e.tags:
|
||||
hit["admin_level"] = get_admin_level(e.tags)
|
||||
hit["element"] = e.osm_id
|
||||
hit["geojson"] = typing.cast(str, e.geojson_str)
|
||||
result = wikidata.build_dict(hit, lat, lon)
|
||||
return {"elements": elements, "result": result}
|
||||
|
||||
query = wikidata.geosearch_query(lat, lon)
|
||||
geo_rows = wikidata.wdqs(query)
|
||||
row = wikidata.filter_geosearch_row(geo_rows)
|
||||
if row:
|
||||
hit = wikidata.commons_from_rows([row])
|
||||
elements = []
|
||||
result = wikidata.build_dict(hit, lat, lon)
|
||||
|
||||
query = wikidata.geosearch_query(lat, lon)
|
||||
|
||||
return {"elements": elements, "result": result, "query": query}
|
||||
|
||||
|
||||
|
|
@ -343,7 +367,8 @@ def index() -> str | Response:
|
|||
if error_msg := validate_coordinates(lat, lon):
|
||||
return jsonify(coords={"lat": lat, "lon": lon}, error=error_msg)
|
||||
|
||||
result = lat_lon_to_wikidata(lat, lon)["result"]
|
||||
needs_commons = request.args.get("needs_commons", "true").lower() != "false"
|
||||
result = lat_lon_to_wikidata(lat, lon, needs_commons=needs_commons)["result"]
|
||||
result.pop("element", None)
|
||||
result.pop("geojson", None)
|
||||
if logging_enabled:
|
||||
|
|
@ -401,7 +426,7 @@ def highlight_sparql(query: str) -> str:
|
|||
app.jinja_env.filters["highlight_sparql"] = highlight_sparql
|
||||
|
||||
|
||||
def build_detail_page(lat: float, lon: float) -> str:
|
||||
def build_detail_page(lat: float, lon: float, needs_commons: bool = True) -> str:
|
||||
"""Run lookup and build detail page."""
|
||||
if lat < -90 or lat > 90 or lon < -180 or lon > 180:
|
||||
error = (
|
||||
|
|
@ -411,7 +436,7 @@ def build_detail_page(lat: float, lon: float) -> str:
|
|||
return render_template("query_error.html", lat=lat, lon=lon, error=error)
|
||||
|
||||
try:
|
||||
reply = lat_lon_to_wikidata(lat, lon)
|
||||
reply = lat_lon_to_wikidata(lat, lon, needs_commons=needs_commons)
|
||||
except wikidata.QueryError as e:
|
||||
query, r = e.args
|
||||
return render_template("query_error.html", lat=lat, lon=lon, query=query, r=r)
|
||||
|
|
@ -429,6 +454,7 @@ def build_detail_page(lat: float, lon: float) -> str:
|
|||
element_id=element,
|
||||
geojson=geojson,
|
||||
css=css,
|
||||
needs_commons=needs_commons,
|
||||
**reply,
|
||||
)
|
||||
|
||||
|
|
@ -453,7 +479,8 @@ def detail_page() -> Response | str:
|
|||
"coordinate_error.html", lat_str=lat_str, lon_str=lon_str, error=error
|
||||
)
|
||||
|
||||
return build_detail_page(lat, lon)
|
||||
needs_commons = request.args.get("needs_commons", "true").lower() != "false"
|
||||
return build_detail_page(lat, lon, needs_commons=needs_commons)
|
||||
|
||||
|
||||
@app.route("/reports")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Geocode to Commons{% endblock %}
|
||||
{% block title %}
|
||||
{%- if result.commons_cat %}{{ result.commons_cat.title }}
|
||||
{%- elif result.wikidata %}{{ result.wikidata }}
|
||||
{%- else %}{{ lat }}, {{ lon }}
|
||||
{%- endif %} — Geocode
|
||||
{% endblock %}
|
||||
|
||||
{% block link %}
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
|
|
@ -15,104 +20,164 @@
|
|||
|
||||
<script>
|
||||
var map = L.map('map').setView([{{ lat }}, {{ lon }}], 13);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
}).addTo(map);
|
||||
|
||||
var marker = L.marker([{{ lat }} , {{ lon }}]).addTo(map);
|
||||
var marker = L.marker([{{ lat }}, {{ lon }}]).addTo(map);
|
||||
|
||||
{% if geojson %}
|
||||
|
||||
L.geoJSON({{ geojson | safe }}).addTo(map);
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block style %}
|
||||
<style>
|
||||
/*
|
||||
#map {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Styles for the map */
|
||||
#map {
|
||||
position: fixed; /* This keeps the map in place when the page is scrolled */
|
||||
top: 0; /* Starting from the top edge of the browser window */
|
||||
right: 0; /* Positioned on the right side */
|
||||
width: 50%; /* Half the screen width */
|
||||
height: 100%; /* Full height of the browser window */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#main {
|
||||
width: 48%
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.tag-key {
|
||||
color: #6c757d;
|
||||
font-size: 0.75em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.element-card {
|
||||
border-left: 3px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
.element-card.matched {
|
||||
border-left-color: #0d6efd !important;
|
||||
background-color: #f8f9ff;
|
||||
}
|
||||
|
||||
details > summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details > summary:hover {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
{{ css | safe }}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="map"></div>
|
||||
<div class="m-3" id="main">
|
||||
<h1>Geocode coordinates to Commons Category</h1>
|
||||
<div class="px-3 py-3" id="main">
|
||||
|
||||
<p>
|
||||
<a href="{{ url_for('index') }}">home</a>
|
||||
|
|
||||
<a href="{{ url_for('index', lat=lat, lon=lon) }}">visit endpoint</a>
|
||||
| <a href="https://www.openstreetmap.org/#map=17/{{lat }}/{{ lon }}">view in OSM</a>
|
||||
{% if result.commons_cat %}
|
||||
| <a href="{{ result.commons_cat.url }}">Commons category</a>
|
||||
{% endif %}
|
||||
<h2 class="mb-0">
|
||||
{%- if result.commons_cat %}{{ result.commons_cat.title }}
|
||||
{%- elif result.wikidata %}{{ result.wikidata }}
|
||||
{%- else %}<span class="text-muted">No result</span>
|
||||
{%- endif %}
|
||||
</h2>
|
||||
<p class="text-muted mb-3" style="font-family:monospace;font-size:0.85em">{{ "%.5f"|format(lat) }}, {{ "%.5f"|format(lon) }}</p>
|
||||
|
||||
{% if result.wikidata %}
|
||||
| <a href="https://www.wikidata.org/wiki/{{ result.wikidata }}">{{ result.wikidata }}</a>
|
||||
{% endif %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="row g-2">
|
||||
{% if result.wikidata %}
|
||||
<div class="col-auto">
|
||||
<span class="tag-key">Wikidata</span>
|
||||
<a href="https://www.wikidata.org/wiki/{{ result.wikidata }}">{{ result.wikidata }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.commons_cat %}
|
||||
<div class="col-auto">
|
||||
<span class="tag-key">Commons category</span>
|
||||
<a href="{{ result.commons_cat.url }}">{{ result.commons_cat.title }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.admin_level %}
|
||||
<div class="col-auto">
|
||||
<span class="tag-key">Admin level</span>
|
||||
{{ result.admin_level }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
| <a href="{{ url_for('detail_page', lat=lat, lon=lon) }}">#</a>
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-sm btn-outline-secondary">Home</a>
|
||||
<a href="{{ url_for('index', lat=lat, lon=lon) }}{% if not needs_commons %}&needs_commons=false{% endif %}" class="btn btn-sm btn-outline-secondary">API endpoint</a>
|
||||
<a href="https://www.openstreetmap.org/#map=17/{{ lat }}/{{ lon }}" class="btn btn-sm btn-outline-secondary">OpenStreetMap</a>
|
||||
{% if result.commons_cat %}
|
||||
<a href="{{ result.commons_cat.url }}" class="btn btn-sm btn-outline-secondary">Commons category</a>
|
||||
{% endif %}
|
||||
{% if result.wikidata %}
|
||||
<a href="https://www.wikidata.org/wiki/{{ result.wikidata }}" class="btn btn-sm btn-outline-secondary">{{ result.wikidata }}</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('detail_page', lat=lat, lon=lon) }}" class="btn btn-sm btn-outline-secondary">#</a>
|
||||
{% if needs_commons %}
|
||||
<a href="{{ url_for('detail_page', lat=lat, lon=lon) }}&needs_commons=false" class="btn btn-sm btn-outline-primary">Try without Commons Category</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('detail_page', lat=lat, lon=lon) }}" class="btn btn-sm btn-outline-primary">Require Commons Category</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</p>
|
||||
<details class="mb-3">
|
||||
<summary class="text-muted small">API response</summary>
|
||||
<pre class="mt-2 p-2 bg-light rounded small">{{ result | tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
|
||||
<h4>API returns</h4>
|
||||
<pre>{{ result | tojson(indent=2) }}</pre>
|
||||
|
||||
{% if result.wikidata %}
|
||||
<p><strong>Wikidata item</strong>: <a href="https://www.wikidata.org/wiki/{{ result.wikidata }}">{{ result.wikidata }}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if result.commons_cat %}
|
||||
<p><strong>Commons category</strong>: <a href="{{ result.commons_cat.url }}">{{result.commons_cat.title }}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% if elements %}
|
||||
<p>{{ elements.count() }} surrounding elements found</p>
|
||||
{% else %}
|
||||
<p>No elements found</p>
|
||||
{% endif %}
|
||||
|
||||
{% if query %}
|
||||
<p>Searching for Wikimedia Commons categories using this SPARQL query. <a href="https://query.wikidata.org/#{{ query | urlencode }}">Wikidata Query service</a></p>
|
||||
<div>
|
||||
{{ query | highlight_sparql | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for element in elements %}
|
||||
{% set tags = element.tags %}
|
||||
<div class="rounded border border-4 p-1 my-2{% if element_id == element.osm_id %} bg-primary-subtle{% endif %}">
|
||||
{% for key, value in element.tags.items() if not (key == "way_area" or "name:" in key or key.startswith("source")) %}
|
||||
<div><strong>{{ key }}</strong>: {{ value }}</div>
|
||||
{% if elements %}
|
||||
{% set elem_count = elements.count() %}
|
||||
<h6 class="text-muted mb-2">{{ elem_count }} surrounding OSM element{{ 's' if elem_count != 1 }}</h6>
|
||||
{% for element in elements %}
|
||||
{% set tags = element.tags %}
|
||||
<div class="card mb-2 element-card{% if element_id == element.osm_id %} matched{% endif %}">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex align-items-start justify-content-between mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ tags.name or ('relation' if element.osm_id < 0 else 'way') ~ ' ' ~ element.osm_id|abs }}
|
||||
</span>
|
||||
<div class="d-flex gap-2 ms-2 flex-shrink-0">
|
||||
{% if tags.wikidata %}
|
||||
<a href="https://www.wikidata.org/wiki/{{ tags.wikidata }}" class="badge bg-secondary text-decoration-none small">{{ tags.wikidata }}</a>
|
||||
{% endif %}
|
||||
<a href="{{ element.osm_url }}" class="badge bg-light text-secondary border text-decoration-none small">OSM</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-auto g-2 small">
|
||||
{% for key, value in tags.items() if not (key == "way_area" or "name:" in key or key.startswith("source") or key == "name" or key == "wikidata") %}
|
||||
<div class="col">
|
||||
<span class="tag-key">{{ key }}</span>{{ value }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted small">No surrounding elements found.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if query %}
|
||||
<details class="mt-3">
|
||||
<summary class="text-muted small">SPARQL geosearch query
|
||||
<a href="https://query.wikidata.org/#{{ query | urlencode }}" class="ms-2 small" onclick="event.stopPropagation()">run on Wikidata ↗</a>
|
||||
</summary>
|
||||
<div class="mt-2">{{ query | highlight_sparql | safe }}</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -2,39 +2,158 @@
|
|||
|
||||
{% block title %}Geocode to Commons{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="m-3">
|
||||
<h1>Geocode coordinates to Commons Category</h1>
|
||||
{% block style %}
|
||||
<style>
|
||||
.step-number {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
<ol>
|
||||
<li>Overpass query to find OSM polygon that contain given lat/lon.</li>
|
||||
<li>Sort list of OSM polygons by admin_level descending.</li>
|
||||
<li>Check each polygon for wikidata tag, starting with highest admin_level first.</li>
|
||||
<li>If the wikidata tag isn't found try looking for a 'ref:gss' tag and look for matching Wikidata item using the
|
||||
Wikidata Query service (WDQS)</li>
|
||||
<li>Finally try match by name. Look for nearby items with the same name using WDQS.</li>
|
||||
<li>Return the most specific Wikidata QID and Commons Category.</li>
|
||||
</ol>
|
||||
.param-name {
|
||||
font-family: monospace;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
<p><a href="{{ url_for("map_page") }}">Interactive map with clickable pin for geocode testing</a>: click on any location within the map to place a pin and see the geocoding results for that specific point.</p>
|
||||
|
||||
<form>
|
||||
Latitude/Longitude: <input name="q"/> (e.g. 54.375, -2.999) <input type="submit" value="go"/>
|
||||
</form>
|
||||
|
||||
<p>Examples</p>
|
||||
|
||||
<ul>
|
||||
{% for lat, lon, name in samples %}
|
||||
<li><a href="detail?lat={{ lat }}&lon={{ lon }}">{{ name }}</a> ({{ lat }}, {{ lon }})
|
||||
—
|
||||
<a href="?lat={{ lat }}&lon={{ lon }}">API call</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% set repo = "https://git.4angle.com/edward/geocode" %}
|
||||
|
||||
<p>source code: <a href="{{ repo }}">{{ repo }}</a></p>
|
||||
</div>
|
||||
.param-value {
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.example-name {
|
||||
min-width: 12rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-lg py-4">
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="mb-1">Geocode to Commons Category</h1>
|
||||
<p class="text-muted mb-3">Convert latitude/longitude to a Wikidata item and Wikimedia Commons category.</p>
|
||||
|
||||
<form class="d-flex gap-2 align-items-center mb-3" action="/">
|
||||
<label for="q" class="text-nowrap text-muted small">Lat, Lon</label>
|
||||
<input id="q" name="q" class="form-control form-control-sm" style="max-width:18rem" placeholder="e.g. 54.375, -2.999">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Go</button>
|
||||
</form>
|
||||
|
||||
<p><a href="{{ url_for("map_page") }}">Interactive map ↗</a> — click any location to geocode it.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-5">
|
||||
<h5 class="mb-3">How it works</h5>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for step in [
|
||||
("Find OSM polygons", "Query PostGIS for all polygons that contain the given point."),
|
||||
("Sort by admin level", "Order polygons by admin_level descending — most specific first."),
|
||||
("Check wikidata tag", "For each polygon, look for a <code>wikidata</code> tag and resolve the Commons category."),
|
||||
("Check ref:gss tag", "If no wikidata tag, try <code>ref:gss</code> and look up the matching Wikidata item via WDQS."),
|
||||
("Match by name", "Try finding a nearby Wikidata item with the same name using WDQS."),
|
||||
("Return result", "Return the most specific Wikidata QID and Commons category found.")
|
||||
] %}
|
||||
<div class="d-flex gap-3 align-items-start">
|
||||
<span class="step-number rounded-circle bg-secondary text-white d-flex align-items-center justify-content-center">{{ loop.index }}</span>
|
||||
<div>
|
||||
<div class="fw-semibold small">{{ step[0] }}</div>
|
||||
<div class="text-muted small">{{ step[1] | safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<h5 class="mb-3">API</h5>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<p class="mb-1"><code>GET /?lat=<lat>&lon=<lon></code></p>
|
||||
<p class="text-muted small mb-3">Returns JSON with the Wikidata item and Commons category for the given coordinates.</p>
|
||||
|
||||
<table class="table table-sm small mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Values</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="param-name">lat</td>
|
||||
<td class="text-muted">decimal degrees</td>
|
||||
<td>Latitude</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="param-name">lon</td>
|
||||
<td class="text-muted">decimal degrees</td>
|
||||
<td>Longitude</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="param-name">needs_commons</td>
|
||||
<td>
|
||||
<span class="param-value">true</span> (default)<br>
|
||||
<span class="param-value">false</span>
|
||||
</td>
|
||||
<td>
|
||||
When <span class="param-value">true</span>, only returns a result if a Wikimedia Commons
|
||||
category can be found. When <span class="param-value">false</span>, returns the best
|
||||
matching Wikidata item even if it has no Commons category — matched by OSM relation or
|
||||
way ID via the Wikidata Query Service.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="text-muted small mb-1">Example response</h6>
|
||||
<pre class="bg-light rounded p-2 small">{
|
||||
"wikidata": "Q184618",
|
||||
"commons_cat": {
|
||||
"title": "County Tipperary",
|
||||
"url": "https://commons.wikimedia.org/wiki/Category:County_Tipperary"
|
||||
},
|
||||
"admin_level": 6,
|
||||
"coords": { "lat": 52.41037, "lon": -7.84651 }
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3">Examples</h5>
|
||||
<table class="table table-sm table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="example-name">Place</th>
|
||||
<th>Coordinates</th>
|
||||
<th colspan="2">Links</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for lat, lon, name in samples %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('detail_page', lat=lat, lon=lon) }}" class="text-decoration-none">{{ name }}</a></td>
|
||||
<td class="text-muted small" style="font-family:monospace">{{ lat }}, {{ lon }}</td>
|
||||
<td><a href="{{ url_for('detail_page', lat=lat, lon=lon) }}" class="small">detail</a></td>
|
||||
<td><a href="{{ url_for('index', lat=lat, lon=lon) }}" class="small">API</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="text-muted small mt-3">
|
||||
Source code: <a href="https://git.4angle.com/edward/geocode">https://git.4angle.com/edward/geocode</a>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
SELECT DISTINCT ?item ?distance ?itemLabel ?isa ?isaLabel ?commonsCat ?commonsSiteLink WHERE {
|
||||
SELECT DISTINCT ?item ?distance ?itemLabel ?isa ?isaLabel ?commonsCat ?commonsSiteLink ?osmRelation ?osmWay WHERE {
|
||||
{
|
||||
SELECT DISTINCT ?item ?location ?distance ?isa WHERE {
|
||||
VALUES ?want { wd:Q486972 wd:Q56061 }
|
||||
|
|
@ -20,5 +20,7 @@ SELECT DISTINCT ?item ?distance ?itemLabel ?isa ?isaLabel ?commonsCat ?commonsSi
|
|||
OPTIONAL { ?item wdt:P373 ?commonsCat. }
|
||||
OPTIONAL { ?commonsSiteLink schema:about ?item;
|
||||
schema:isPartOf <https://commons.wikimedia.org/>. }
|
||||
OPTIONAL { ?item wdt:P402 ?osmRelation. }
|
||||
OPTIONAL { ?item wdt:P10689 ?osmWay. }
|
||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
||||
} ORDER BY (?distance)
|
||||
|
|
|
|||
10
templates/sparql/lookup_by_osm_relation.sparql
Normal file
10
templates/sparql/lookup_by_osm_relation.sparql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
SELECT ?item ?itemLabel ?commonsCat ?commonsSiteLink ?osmRelation WHERE {
|
||||
VALUES ?osmRelation {
|
||||
{% for id in relation_ids %}"{{ id }}" {% endfor %}
|
||||
}
|
||||
?item wdt:P402 ?osmRelation .
|
||||
OPTIONAL { ?item wdt:P373 ?commonsCat. }
|
||||
OPTIONAL { ?commonsSiteLink schema:about ?item ;
|
||||
schema:isPartOf <https://commons.wikimedia.org/>. }
|
||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue