Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion internal/commands/traffic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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))
Expand Down
26 changes: 26 additions & 0 deletions internal/commands/traffic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}