fix: normalization of city name address

This commit is contained in:
2026-05-28 23:23:22 +02:00
parent 5c004f5e2f
commit edda183c16
8 changed files with 216 additions and 50 deletions
+40
View File
@@ -10,6 +10,7 @@ import (
"strings"
"sync"
"time"
"unicode"
)
// ErrNotFound is returned by providers when a callsign is unknown.
@@ -100,6 +101,7 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
// corrected entity mapping (e.g. Sicily → Italy) heal stale cached
// rows without waiting for the TTL to expire.
fillFromDXCC(&r, dxcc)
normalizeNames(&r)
return r, nil
}
@@ -111,6 +113,7 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
r.Source = p.Name()
r.FetchedAt = time.Now().UTC()
fillFromDXCC(&r, dxcc)
normalizeNames(&r)
_ = m.cache.Put(ctx, r)
return r, nil
}
@@ -145,6 +148,43 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
return Result{}, lastErr
}
// normalizeNames title-cases the human-readable text fields so a QRZ/HamQTH
// reply in ALL CAPS ("NOEL CHENAVARD", "VETRAZ-MONTHOUX") is stored and shown
// consistently ("Noel Chenavard", "Vetraz-Monthoux"). State/zones/grid are
// left untouched (codes like CT must stay as-is).
func normalizeNames(r *Result) {
r.Name = titleCase(r.Name)
r.QTH = titleCase(r.QTH)
r.Address = titleCase(r.Address)
}
// titleCase lowercases the whole string then capitalises the first letter of
// each word. Word boundaries are any non-alphanumeric rune (space, hyphen,
// apostrophe, slash…), so "vetraz-monthoux" → "Vetraz-Monthoux" and
// "o'brien" → "O'Brien". Digits never get a leading capital ("74140" stays).
func titleCase(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
runes := []rune(strings.ToLower(s))
atWordStart := true
for i, r := range runes {
switch {
case unicode.IsLetter(r):
if atWordStart {
runes[i] = unicode.ToUpper(r)
}
atWordStart = false
case unicode.IsDigit(r):
atWordStart = false
default:
atWordStart = true
}
}
return string(runes)
}
// fillFromDXCC fills (or overrides) country/continent/zones/lat/lon from
// the cty.dat resolver. cty.dat is the authoritative source for DXCC
// mapping, so Country/Continent/CQZ/ITUZ are ALWAYS overridden when it
+21
View File
@@ -0,0 +1,21 @@
package lookup
import "testing"
func TestTitleCase(t *testing.T) {
cases := map[string]string{
"NOEL CHENAVARD": "Noel Chenavard",
"VETRAZ-MONTHOUX": "Vetraz-Monthoux",
"o'brien": "O'Brien",
"866 ROUTE DES VOIRONS": "866 Route Des Voirons",
"PARIS": "Paris",
"": "",
" saint-étienne ": "Saint-Étienne",
"JOHN": "John",
}
for in, want := range cases {
if got := titleCase(in); got != want {
t.Errorf("titleCase(%q) = %q, want %q", in, got, want)
}
}
}