feat: the offline FCC database answers during entry, not only at save

ULS was wired into AddQSO alone, so an operator who downloaded it and has no
QRZ.com/HamQTH account saw nothing while typing a US call: cty.dat gives the
country, the zones, and a 4-character grid that is the ENTITY centroid — a
thousand kilometres from the station. The county and the real square were stamped
after logging, too late to point an antenna with.

The enrichment runs last in the lookup wrapper and only fills blanks, so a
provider always wins. ULS carries no name and no address, so it completes a QRZ
record and can never replace one.

The grid needed a rule of its own. refineGrid keeps what is there unless the new
value extends it — correct for a QRZ square, wrong against an entity centroid,
which is simply a different square. A per-callsign FCC square therefore beats any
4-character grid, while a 6-character one already present is left untouched.
Lat/lon are recomputed only when we actually moved the grid.

Portable calls are skipped: W1AW/4 is not what the FCC licensed.
This commit is contained in:
2026-07-28 22:10:22 +02:00
parent 01bcf256e2
commit 29591c5f0c
3 changed files with 128 additions and 0 deletions
+66
View File
@@ -6370,9 +6370,75 @@ func (a *App) lookupCallsign(callsign string, force bool) (lookup.Result, error)
}
}
}
a.enrichFromULS(&r, callsign)
return r, err
}
// enrichFromULS fills a US station's state, county and grid from the offline FCC
// database while the operator is still typing.
//
// The ULS data was only ever applied at SAVE time (applyULSCounty), so an
// operator with the database downloaded and no QRZ.com/HamQTH account saw
// nothing but cty.dat during entry — country, zones, and a 4-character grid that
// is the ENTITY centroid, thousands of km off. The county and a 6-character grid
// were stamped silently after logging, too late to steer an antenna by.
//
// It runs LAST and only fills blanks, so an online provider always wins: ULS
// holds no name and no address, so it can complete a QRZ record but never
// replace one. The grid goes through refineGrid, which accepts the ULS square
// only when it EXTENDS what is already there (JN → JN05JG) and never when it
// would contradict it.
func (a *App) enrichFromULS(r *lookup.Result, callsign string) {
if a.uls == nil {
return
}
switch r.DXCC {
case 291, 110, 6: // United States, Hawaii, Alaska
default:
return
}
call := strings.ToUpper(strings.TrimSpace(callsign))
if r.Callsign != "" {
call = strings.ToUpper(strings.TrimSpace(r.Callsign))
}
if call == "" || strings.Contains(call, "/") {
// A portable call is not what the FCC licensed — W1AW/4 is not a row.
return
}
loc, ok := a.uls.Resolve(call)
if !ok {
return
}
if strings.TrimSpace(r.State) == "" {
r.State = loc.State
}
if strings.TrimSpace(r.County) == "" {
r.County = loc.CNTY()
}
// Grid. refineGrid keeps the ULS square when it extends what is there. It
// does NOT cover the case this feature exists for: cty.dat's 4-character
// square is the ENTITY centroid, so for a US call it is usually a different
// square altogether — refineGrid would keep it, and the operator would go on
// pointing an antenna at the middle of the country. A per-callsign ULS square
// beats any 4-character one, whatever its letters; a 6-character grid already
// present (QRZ) is left alone.
newGrid := ""
if g := refineGrid(r.Grid, loc.Grid); g != "" && g != r.Grid {
newGrid = g
} else if len(strings.TrimSpace(r.Grid)) <= 4 && len(strings.TrimSpace(loc.Grid)) >= 6 {
newGrid = loc.Grid
}
if newGrid != "" {
r.Grid = newGrid
// Lat/lon follow, or distance and azimuth would still be computed from the
// centroid the grid no longer says. Only when WE changed the grid: a
// provider's own coordinates are more precise than a square's centre.
if lat, lon, ok := gridToLatLon(newGrid); ok {
r.Lat, r.Lon = lat, lon
}
}
}
// OpenExternalURL opens a URL in the user's default browser. Wails ships
// runtime.BrowserOpenURL for exactly this — used by the QRZ.com icon
// next to the callsign field, the future Clublog/HamQTH shortcuts, etc.