fix(dxcc): /MM and /AM belong to no entity

RI1FJL/MM resolved to Franz Josef Land while the expedition was still sailing
there, telling the operator they had worked an entity they had not.

The old behaviour was deliberate — the comment read "strict DXCC says no
entity, but the log should still show the operator's country" — and that is
wrong for exactly this reason. A home country on a maritime mobile is not extra
information; it is a false claim about where the contact happened, and it is
the kind of false claim that gets a QSO submitted for an award it cannot win.

Trailing only. A LEADING "MM" is the Scotland prefix and "AM" is Spain, so
MM0ABC and AM5X keep their entities — getting that wrong would be a far larger
error than the one being fixed. /P, /M and /B are unaffected: a portable
station is still ashore.

In the cluster the column says "Maritime mobile — no DXCC" rather than going
blank, because blank is what an UNRESOLVED spot looks like and the two mean
opposite things — one is "we don't know yet", this is "there is nothing to
know". Display only: the QSO's own Country stays empty, since that field holds
a DXCC entity name and there is none.
This commit is contained in:
2026-08-13 12:25:33 +02:00
parent dc0b00b474
commit 4ba5569ca8
6 changed files with 443 additions and 361 deletions
+10
View File
@@ -17452,6 +17452,16 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
if a.dxcc == nil { if a.dxcc == nil {
continue continue
} }
// Say WHY a maritime/aeronautical mobile has no entity, rather than
// leaving the column blank — blank is what an unresolved spot looks like,
// and the two mean opposite things: one is "we don't know yet", this is
// "there is nothing to know, the station is at sea". Display only: the
// QSO's own Country stays empty, because it is a DXCC entity name and
// there is none.
if dxcc.IsMobileNoEntity(q.Call) {
out[i].Country = "Maritime mobile — no DXCC"
continue
}
m, ok := a.dxcc.Lookup(q.Call) m, ok := a.dxcc.Lookup(q.Call)
if !ok || m.Entity == nil { if !ok || m.Entity == nil {
continue continue
+4 -2
View File
@@ -10,7 +10,8 @@
"Station Control: the short and long path headings are repeated under the compass, at a readable size and clickable.", "Station Control: the short and long path headings are repeated under the compass, at a readable size and clickable.",
"Compact mode: the window now fits the entry strip instead of leaving a band of empty space below it.", "Compact mode: the window now fits the entry strip instead of leaving a band of empty space below it.",
"HRDLog: an ON AIR option publishes your live frequency, mode and rig on hrdlog.net.", "HRDLog: an ON AIR option publishes your live frequency, mode and rig on hrdlog.net.",
"ADIF: the county is exported as STATE,COUNTY as the standard requires, and a county imported that way now fills the state too." "ADIF: the county is exported as STATE,COUNTY as the standard requires, and a county imported that way now fills the state too.",
"A /MM or /AM callsign no longer resolves to a country — RI1FJL/MM read as Franz Josef Land while the expedition was still sailing there."
], ],
"fr": [ "fr": [
"Cluster : « Masquer les contactés » ne masque plus un spot qui est un nouveau préfixe, comté, carré ou parc dans une contrée déjà faite.", "Cluster : « Masquer les contactés » ne masque plus un spot qui est un nouveau préfixe, comté, carré ou parc dans une contrée déjà faite.",
@@ -20,7 +21,8 @@
"Contrôle station : les azimuts court et long chemin sont repris sous la boussole, lisibles et cliquables.", "Contrôle station : les azimuts court et long chemin sont repris sous la boussole, lisibles et cliquables.",
"Mode compact : la fenêtre épouse la bande de saisie au lieu de laisser une bande vide en dessous.", "Mode compact : la fenêtre épouse la bande de saisie au lieu de laisser une bande vide en dessous.",
"HRDLog : une option ON AIR publie ta fréquence, ton mode et ta radio en direct sur hrdlog.net.", "HRDLog : une option ON AIR publie ta fréquence, ton mode et ta radio en direct sur hrdlog.net.",
"ADIF : le comté est exporté sous la forme ÉTAT,COMTÉ comme l exige le standard, et un comté importé ainsi remplit aussi l état." "ADIF : le comté est exporté sous la forme ÉTAT,COMTÉ comme l exige le standard, et un comté importé ainsi remplit aussi l état.",
"Un indicatif en /MM ou /AM ne se résout plus en pays — RI1FJL/MM affichait Franz Josef Land alors que l expédition faisait encore route."
] ]
}, },
{ {
+31
View File
@@ -132,6 +132,18 @@ func (db *DB) Entities() []*Entity {
func (db *DB) Lookup(callsign string) (Match, bool) { func (db *DB) Lookup(callsign string) (Match, bool) {
db.mu.RLock() db.mu.RLock()
defer db.mu.RUnlock() defer db.mu.RUnlock()
// Maritime and aeronautical mobile belong to NO entity, and that is not a
// technicality — the station is at sea or in the air. RI1FJL/MM resolved to
// Franz Josef Land while the expedition was still on its way there, telling
// the operator they had worked an entity they had not.
//
// The previous choice was deliberate ("the log should still show the
// operator's country") and it is wrong for exactly that reason: a home
// country here is not extra information, it is a false claim about where the
// contact happened.
if IsMobileNoEntity(callsign) {
return Match{}, false
}
call := normalizeCallsign(callsign) call := normalizeCallsign(callsign)
if call == "" { if call == "" {
return Match{}, false return Match{}, false
@@ -364,3 +376,22 @@ func replaceAreaDigit(call string, d byte) string {
} }
return call return call
} }
// IsMobileNoEntity reports a callsign DXCC assigns to no entity: a TRAILING
// /MM (maritime mobile) or /AM (aeronautical mobile).
//
// Trailing only. A LEADING "MM" is the Scotland prefix and "AM" is Spain, so
// MM0ABC and AM5X are ordinary calls — treating those as entity-less would be a
// far larger error than the one this fixes.
func IsMobileNoEntity(callsign string) bool {
s := strings.ToUpper(strings.TrimSpace(callsign))
i := strings.LastIndex(s, "/")
if i < 0 {
return false
}
switch s[i+1:] {
case "MM", "AM":
return true
}
return false
}
+39
View File
@@ -0,0 +1,39 @@
package dxcc
import "testing"
// /MM and /AM belong to NO DXCC entity: the station is at sea or in the air.
//
// This is not a technicality. RI1FJL/MM resolved to Franz Josef Land while the
// expedition was still sailing there, which told the operator they had worked
// an entity they had not.
func TestMobileNoEntity(t *testing.T) {
for _, tc := range []struct {
call string
want bool
}{
{"RI1FJL/MM", true},
{"YB1SCY/AM", true},
{"ri1fjl/mm", true},
{" RI1FJL/MM ", true},
// A LEADING MM is Scotland and AM is Spain. Treating those as
// entity-less would be a far larger error than the one this fixes.
{"MM0ABC", false},
{"AM5X", false},
{"MM/F4BPO", false},
{"AM/DL1ABC", false},
// Other suffixes keep their entity — a portable station is still ashore.
{"F4BPO/P", false},
{"F4BPO/M", false},
{"4U1UN/B", false},
{"F4BPO", false},
// Only the LAST element counts: RI1FJL/MM/P is still maritime, and a
// call whose /MM is not final is not.
{"RI1FJL/MM/P", false},
{"", false},
} {
if got := IsMobileNoEntity(tc.call); got != tc.want {
t.Errorf("IsMobileNoEntity(%q) = %v, want %v", tc.call, got, tc.want)
}
}
}