From 7da704d8376303c3b30291c4fea131934928766a Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 15 Aug 2026 12:09:24 +0100 Subject: [PATCH] Skip choice for unique postcode matches --- README.md | 1 + src/uk_bus_stops/static/app.js | 9 +++++++++ src/uk_bus_stops/templates/about.html | 2 +- tests/test_uk_bus_stops_playwright.py | 25 +++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f86d44..9a33fa7 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ the search box. Selecting a stop shows its tags and stop-area-aware bus route relations. Stop results identify their transport mode (such as bus, rail or tram), and moving the map at zoom level 15 or closer loads stops in the visible area. Wider map views deliberately do not query Overpass. +Postcode searches with exactly one Nominatim match skip the choice screen. The current map bounds bias Nominatim's ranking without excluding UK matches outside the visible area. diff --git a/src/uk_bus_stops/static/app.js b/src/uk_bus_stops/static/app.js index 6bb67a9..58a2535 100644 --- a/src/uk_bus_stops/static/app.js +++ b/src/uk_bus_stops/static/app.js @@ -51,6 +51,11 @@ function parseCoordinates(value) { return {lat, lon}; } +/** Return whether text has the shape of a complete UK postcode. */ +function isUkPostcode(value) { + return /^(?:GIR\s*0AA|[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2})$/i.test(value.trim()); +} + /** Put the current search in the address bar without reloading the page. */ function setSearchUrl(params, replace = false) { const query = params.toString(); @@ -305,6 +310,10 @@ function renderStops(data, fitMap = true, sortOrigin = null, preserveSelection = /** Show UK-only Nominatim matches and wait for the user to choose one. */ function renderGeocodeChoices(data) { + if (data.locations.length === 1 && isUkPostcode(data.query)) { + chooseLocation(data.locations[0]); + return; + } selectedStop = null; markers.clearLayers(); if (searchMarker) { searchMarker.remove(); searchMarker = null; } diff --git a/src/uk_bus_stops/templates/about.html b/src/uk_bus_stops/templates/about.html index 4a22c57..3582235 100644 --- a/src/uk_bus_stops/templates/about.html +++ b/src/uk_bus_stops/templates/about.html @@ -40,7 +40,7 @@ q - A postcode, street, place name, or exact ATCO code. Place searches show up to 20 UK-only matches to choose from. + A postcode, street, place name, or exact ATCO code. Place searches show up to 20 UK-only matches to choose from; a postcode with one match opens directly. {{ url_for('index') }}?q=Bristol+Temple+Meads diff --git a/tests/test_uk_bus_stops_playwright.py b/tests/test_uk_bus_stops_playwright.py index 892e3d7..c885427 100644 --- a/tests/test_uk_bus_stops_playwright.py +++ b/tests/test_uk_bus_stops_playwright.py @@ -191,6 +191,31 @@ def test_coordinate_url_and_search_input(chromium_browser: Any, flask_url: str) page.close() +def test_single_postcode_match_opens_directly(chromium_browser: Any, flask_url: str) -> None: + """A unique UK postcode result skips the location-choice panel.""" + page = chromium_browser.new_page(viewport={"width": 1280, "height": 800}) + page.route("**/api/search?*", lambda route: route.fulfill(json={ + "kind": "geocode", + "query": "SW1A 1AA", + "locations": [{ + "lat": 51.501, "lon": -0.142, "label": "Westminster, London", + "type": "postcode", + }], + })) + page.route("**/api/stops?*", lambda route: route.fulfill(json={ + "kind": "location", + "location": {"lat": 51.501, "lon": -0.142, "label": "Westminster, London"}, + "stops": [], + })) + page.goto(flask_url, wait_until="networkidle") + page.get_by_label("Location or ATCO code").fill("SW1A 1AA") + page.get_by_role("button", name="Search").click() + playwright_api.expect(page).to_have_url(re.compile(r"\?lat=51.501&lon=-0.142$")) + playwright_api.expect(page.locator("#stop-count")).to_have_text("0 found") + playwright_api.expect(page.locator("#geocode-panel")).to_be_hidden() + page.close() + + def test_mobile_layout_opens_results_sheet(chromium_browser: Any, flask_url: str) -> None: """The finder exposes its search panel as an open mobile bottom sheet.""" page = chromium_browser.new_page(viewport={"width": 390, "height": 844})