fix(lookup): /QRP is about power, not about place

QRZ has no record under M0BFS/QRP, so the lookup falls back to the home call —
that part worked. What followed did not: the home record's location is then
discarded, on the reasoning that a portable operator is not at their registered
address. True for /P and /M, and simply wrong for /QRP, which says something
about the transmitter and nothing about where it is.

So the grid came back empty for M0BFS/QRP while the same call without the suffix
answered perfectly, which is exactly how it was reported.

The distinction is now explicit rather than lumped in with the other operational
suffixes. /P and /M keep clearing the location, because mobile and portable both
mean somewhere other than home — that behaviour was correct and is untouched.
A power suffix stacked on a portable one (F4BPO/P/QRP) still clears: one of them
moved the operator.
This commit is contained in:
2026-08-11 21:12:37 +02:00
parent c5e0ec9033
commit 344e8b2091
3 changed files with 72 additions and 8 deletions
+41 -6
View File
@@ -176,12 +176,20 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
r.Callsign = call
r.Source = p.Name()
r.FetchedAt = time.Now().UTC()
// The home record's location is the operator's HOME, not where they
// are portable now — clear it so cty.dat fills the real entity.
r.Country, r.Continent = "", ""
r.CQZ, r.ITUZ, r.DXCC = 0, 0, 0
r.Lat, r.Lon = 0, 0
r.Grid, r.State, r.County = "", "", ""
// The home record's location is the operator's HOME — clear it so
// cty.dat fills in where they actually are.
//
// UNLESS the suffix says nothing about location. /QRP is a statement
// about power, not about place: M0BFS/QRP is M0BFS, at home, running
// five watts. Wiping the grid there threw away the one field the
// operator was looking the call up for, and it came back empty while
// the same lookup without the suffix answered perfectly.
if !saysNothingAboutLocation(call) {
r.Country, r.Continent = "", ""
r.CQZ, r.ITUZ, r.DXCC = 0, 0, 0
r.Lat, r.Lon = 0, 0
r.Grid, r.State, r.County = "", "", ""
}
fillFromDXCC(&r, dxcc) // entity/zones/lat-lon from the FULL (slashed) call
normalizeNames(&r)
_ = m.cache.Put(ctx, r)
@@ -232,6 +240,33 @@ var LogSink = func(string, ...any) {}
// right and must be looked up exactly as entered.
var opSuffixes = map[string]bool{"M": true, "MM": true, "AM": true, "P": true, "QRP": true}
// nonLocationSuffixes say nothing about WHERE the operator is.
//
// /QRP is a statement about power. /M and /P and their kin are not: mobile and
// portable both mean "somewhere other than the home station", which is exactly
// why the home record's location is discarded for them. Keeping that distinction
// is the difference between a grid that is stale and a grid that is absent.
var nonLocationSuffixes = map[string]bool{"QRP": true}
// saysNothingAboutLocation reports a call whose every suffix leaves the operator
// at their registered address — so the home record's location can be trusted.
func saysNothingAboutLocation(call string) bool {
parts := strings.Split(strings.ToUpper(strings.TrimSpace(call)), "/")
if len(parts) < 2 {
return false
}
base := strings.TrimSpace(parts[0])
if len(base) < 3 || !strings.ContainsAny(base, "0123456789") {
return false // "JW/OR1A": the first part is a prefix — a location change
}
for _, p := range parts[1:] {
if !nonLocationSuffixes[strings.TrimSpace(p)] {
return false
}
}
return true
}
// stripOpSuffix returns the bare callsign when call carries nothing but
// operational suffixes ("F4LYI/M" → "F4LYI", true). Reports false for anything
// that changes entity or area ("JW/OR1A", "F4BPO/8"), and for a call whose base