The packets page shows one name for an ambiguous path hop, with no indication that it is a guess, and it picks that name without using the geographic filtering the resolver already implements. In the case below the displayed repeater is 185 km from the observer, while the evidence in the database points at a different candidate.
hop-resolver.js computes the ambiguity correctly. packets.js throws it away.
What a user sees
A path hop renders as a single repeater name, identical in weight to an unambiguous one. Nothing says "1 of 9". An operator reading a path has no way to tell a certain hop from a coin flip, and acts on the wrong one.
Worked case from live data
A GRP_TXT flood observed on 2026-09-30, path ["EF","E7","EE","05","7E","AE","44","38"], observed by BE-BGS-RRY120-OBS (IATA OST).
Nine nodes share the prefix EF:
| node |
role |
distance from the transmitter |
distance from OST |
| BE-9041-REP-VER |
repeater |
0.0 km |
~62 km |
💀RRY6710yagiPnshr |
repeater |
10.6 km |
~55 km |
| BE-BLZ-WALTWILDER-EDG |
repeater |
126.0 km |
~185 km |
| BE-BRE-ON8AR🔋 |
repeater |
126.5 km |
~185 km |
| DE-NW-D-R01 |
repeater |
210.4 km |
|
| Dui_Rahm |
repeater |
211.9 km |
|
| D-WAN-Eickel (Test) |
repeater |
240.0 km |
|
| LDN-SM4 📡 / sapphire |
repeater |
280.1 km |
|
| 🐧 |
companion |
no GPS |
|
The UI shows BE-BLZ-WALTWILDER-EDG.
The evidence available at that moment points elsewhere. The next hop in the path is E7, and of the nine EF candidates exactly one has a recorded neighbour with prefix E7:
BE-9041-REP-VER neighbours=7 with prefix E7: 1 (BE-KOU-ON6BW-Repeater, 104 observations)
BE-BLZ-WALTWILDER-EDG neighbours=33 with prefix E7: 0
💀RRY67~10yagi~Pnshr neighbours=10 with prefix E7: 0
So graph affinity, geography and the transmitter's own position all agree on BE-9041-REP-VER, and the page shows the one candidate none of them support.
Root cause
public/hop-resolver.js exposes:
function resolve(hops, originLat, originLon, observerLat, observerLon, observerId)
public/packets.js:966-975 calls it with one argument out of six:
async function resolveHops(hops) {
const unknown = hops.filter(h => !(h in hopNameCache));
if (unknown.length) {
await ensureHopResolver();
const resolved = HopResolver.resolve(unknown);
Object.assign(hopNameCache, resolved || {});
Two consequences:
1. The geographic filter never runs. observerId is undefined, so packetIata is null (hop-resolver.js:189), so nodeInRegion() is never called and regional stays empty. The REGION_RADIUS_KM = 300 filter that would have removed the German and UK candidates is dead code on this path, and the candidate list is used in index order.
2. The ambiguity is discarded. resolve() returns ambiguous: true and a populated conflicts array for a multi-candidate hop (hop-resolver.js:217-227). Object.assign(hopNameCache, resolved) keeps only what the render reads, and nothing in packets.js references conflicts or ambiguous:
$ grep -rn '\.conflicts\|\.ambiguous' public/*.js | grep -v hop-resolver.js
public/analytics.js:461, 463, 472, 2750, 3310, 3311, 3339, 3373, 3374, 3383
Analytics alone. It renders a warning icon per ambiguous edge, counts "Ambiguous Prefixes" as a stat, and offers a confidence filter that can hide ambiguous edges. The pattern this issue asks for already exists in the codebase, one page over.
3. The cache is keyed by prefix alone, so the first packet to resolve EF fixes that name for every later packet regardless of observer or region. packets.js:1001 already carries the comment // Use per-packet cache key if observer context available (ambiguous hops differ by region), so this was known.
On whether it is fixable
One byte gives 256 values for 2043 nodes, so collisions are not going away: EF alone has nine. Perfect resolution is not the goal and is not possible.
What is possible, in increasing order of effort:
- Stop presenting a guess as a fact. Mark an ambiguous hop and show the candidate count. The data is already returned and already discarded. This alone would have prevented the wrong conclusion in the case above.
- Pass the arguments that exist.
resolveHops has the observer on the packet (observer_id, observer_iata) and the transmitter's position where known. Passing them switches on the geographic filter that is already written.
- Key the cache per observer/region rather than per prefix, as the existing comment proposes.
- Use the next resolved hop.
pickByAffinity already scores on graph edges and would have chosen correctly here; it needs neighbouring context the current call does not supply.
Steps 1 and 2 are small and independent, and either would have produced a defensible answer in this case.
Related
The DRY point raised on #2089 applies here too: this is another site resolving a path hash prefix with its own rules. Whatever is done here should not become a sixth variant.
The packets page shows one name for an ambiguous path hop, with no indication that it is a guess, and it picks that name without using the geographic filtering the resolver already implements. In the case below the displayed repeater is 185 km from the observer, while the evidence in the database points at a different candidate.
hop-resolver.jscomputes the ambiguity correctly.packets.jsthrows it away.What a user sees
A path hop renders as a single repeater name, identical in weight to an unambiguous one. Nothing says "1 of 9". An operator reading a path has no way to tell a certain hop from a coin flip, and acts on the wrong one.
Worked case from live data
A GRP_TXT flood observed on 2026-09-30, path
["EF","E7","EE","05","7E","AE","44","38"], observed byBE-BGS-RRY120-OBS(IATAOST).Nine nodes share the prefix
EF:10yagiPnshrThe UI shows
BE-BLZ-WALTWILDER-EDG.The evidence available at that moment points elsewhere. The next hop in the path is
E7, and of the nineEFcandidates exactly one has a recorded neighbour with prefixE7:So graph affinity, geography and the transmitter's own position all agree on
BE-9041-REP-VER, and the page shows the one candidate none of them support.Root cause
public/hop-resolver.jsexposes:public/packets.js:966-975calls it with one argument out of six:Two consequences:
1. The geographic filter never runs.
observerIdisundefined, sopacketIataisnull(hop-resolver.js:189), sonodeInRegion()is never called andregionalstays empty. TheREGION_RADIUS_KM = 300filter that would have removed the German and UK candidates is dead code on this path, and the candidate list is used in index order.2. The ambiguity is discarded.
resolve()returnsambiguous: trueand a populatedconflictsarray for a multi-candidate hop (hop-resolver.js:217-227).Object.assign(hopNameCache, resolved)keeps only what the render reads, and nothing inpackets.jsreferencesconflictsorambiguous:Analytics alone. It renders a warning icon per ambiguous edge, counts "Ambiguous Prefixes" as a stat, and offers a confidence filter that can hide ambiguous edges. The pattern this issue asks for already exists in the codebase, one page over.
3. The cache is keyed by prefix alone, so the first packet to resolve
EFfixes that name for every later packet regardless of observer or region.packets.js:1001already carries the comment// Use per-packet cache key if observer context available (ambiguous hops differ by region), so this was known.On whether it is fixable
One byte gives 256 values for 2043 nodes, so collisions are not going away:
EFalone has nine. Perfect resolution is not the goal and is not possible.What is possible, in increasing order of effort:
resolveHopshas the observer on the packet (observer_id,observer_iata) and the transmitter's position where known. Passing them switches on the geographic filter that is already written.pickByAffinityalready scores on graph edges and would have chosen correctly here; it needs neighbouring context the current call does not supply.Steps 1 and 2 are small and independent, and either would have produced a defensible answer in this case.
Related
The DRY point raised on #2089 applies here too: this is another site resolving a path hash prefix with its own rules. Whatever is done here should not become a sixth variant.