From 01c3e66989ddf4f3a258b992a406e7d99f6d3c88 Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:47:40 +0200 Subject: [PATCH] Stop traffic search matching alphabetic terms inside words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Place queries like "auer" were matching "Stützmauern" via substring. Require a word-boundary prefix so municipalities match as words while cycle aliases and SS12/SS 12 spelling keep working. Refs #12. --- CHANGELOG.md | 8 ++++++++ internal/commands/traffic.go | 16 +++++++++++++++- internal/commands/traffic_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a954204..0a19efb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ SPDX-License-Identifier: CC0-1.0 All notable changes to `odh-cli` are documented here. +## Unreleased + +- `traffic search` no longer matches alphabetic terms inside other words. + Searching `auer` used to hit every `Stützmauern` notice; terms now match at a + word boundary (prefix still allowed, so cycle aliases like `radweg` keep + finding `Radrouten`). Road-number spelling (`SS12` / `SS 12`) is unchanged. + Refs #12. + ## v0.6.1 - 2026-08-04 - Removed a helper left behind by the v0.6.0 zone-filter refactor. It was dead diff --git a/internal/commands/traffic.go b/internal/commands/traffic.go index e3b6695..5fb5b47 100644 --- a/internal/commands/traffic.go +++ b/internal/commands/traffic.go @@ -757,7 +757,10 @@ func trafficSearchMatches(event trafficEvent, search string) bool { if term == "" { continue } - if strings.Contains(haystack, term) || slices.Contains(identifiers, term) { + // Alphabetic terms match at a word boundary only (prefix of a word is + // fine: cycle aliases like "radweg" must still find "Radrouten"). An + // infix match would turn "auer" into every "Stützmauern". + if trafficTextHasTerm(haystack, term) || slices.Contains(identifiers, term) { matched = true break } @@ -773,6 +776,17 @@ func trafficSearchMatches(event trafficEvent, search string) bool { return true } +// trafficTextHasTerm reports whether term sits at the start of any word in +// haystack. Prefixes are allowed; infix matches inside a longer word are not. +func trafficTextHasTerm(haystack, term string) bool { + for _, word := range strings.Fields(haystack) { + if strings.HasPrefix(word, term) { + return true + } + } + return false +} + func trafficSearchTermGroups(search string) [][]string { terms := joinRoadTokens(strings.Fields(normalizeTrafficSearchText(search))) groups := make([][]string, 0, len(terms)) diff --git a/internal/commands/traffic_test.go b/internal/commands/traffic_test.go index fd4a53c..3927cdc 100644 --- a/internal/commands/traffic_test.go +++ b/internal/commands/traffic_test.go @@ -60,3 +60,29 @@ func TestTrafficSearchCycleAliasesAgree(t *testing.T) { } } } + +// Place-name searches must not match inside unrelated words. "auer" is a real +// Unterland municipality; matching "Stützmauern" made the content search lie. +func TestTrafficSearchRequiresWordBoundary(t *testing.T) { + wall := trafficEvent{Place: "Bei Moos im Bereich Stuller Wasserfall: Stützmauern erneuern"} + if trafficSearchMatches(wall, "auer") { + t.Error("auer must not match inside Stützmauern") + } + town := trafficEvent{Place: "Auer: Baustelle auf der Hauptstrasse"} + if !trafficSearchMatches(town, "auer") { + t.Error("auer must still match the municipality name as a word") + } + // Cycle aliases stay prefix matches at a word boundary: "radweg" finds + // "Radrouten", and road numbers keep their own spelling path. + routes := trafficEvent{Place: "Die Radrouten und die ciclabile bei Auer sind gesperrt"} + if !trafficSearchMatches(routes, "radweg") { + t.Error("radweg must still prefix-match Radrouten") + } + if !trafficSearchMatches(routes, "ciclabil") { + t.Error("ciclabil must still prefix-match ciclabile") + } + road := trafficEvent{Place: "Kreuzung mit der SS 12 Brennerstaatsstrasse"} + if !trafficSearchMatches(road, "ss12") { + t.Error("ss12 must still match spaced road numbers") + } +}