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:
Edward Betts 2026-04-18 20:22:09 +01:00
parent 90b009fc90
commit cd9d8779d3
6 changed files with 385 additions and 138 deletions

View file

@ -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: '&copy; <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 %}
L.geoJSON({{ geojson | safe }}).addTo(map);
{% 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 %}
| <a href="{{ url_for('detail_page', lat=lat, lon=lon) }}">#</a>
</p>
<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>
{% endfor %}
<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>
{% endfor %}
<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>
<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>
{% 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>
</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 %}