4fd18ab7854e6620576499d1b7f70fd03d9b6f9d
max
  Thu Aug 27 09:03:04 2026 -0700
hgBlat: name a BLAT track after the gene whose exon it hits, and call protein lengths aa

#Preview2 week - bugs introduced now will need a build patch to fix
topHitLocusLabel() took whatever locusName row hRangeQuery happened to return
first, which is bin order and unrelated to the hit.  A 400 bp query with 345 bp
inside an EGFR exon came back as "400bp chr7:55019001" because the intergenic
row upstream sorted first.  It now walks all overlapping rows and prefers an
exon row over an intron row, breaking ties by how much of the hit each row
covers.

Protein searches were also labelled "154bp SOD1".  Query lengths of a protein
search are amino acids, so the track name, its description and the results page
(length and the coverage mouseover) now say "aa" for those.

refs #38086

diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index 72889cd77bc..ff1122b26a3 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -66,54 +66,60 @@
 }
 
 function blatScoreCell(hit, maxScore) {
     // Score with a little bar chart after it, scaled to the highest score in this result set.
     var pct = maxScore > 0 ? (hit.score / maxScore * 100) : 0;
     return `<span class="blatScoreWrap"><span class="blatScoreVal">${blatFmt(hit.score)}</span>` +
         `<span class="blatScoreBar"><i style="width:${pct.toFixed(1)}%"></i></span></span>`;
 }
 
 function blatIdentityCell(hit) {
     // Just the percentage now (the bar chart moved to the Score column), kept in its semantic color.
     var c = blatIdColor(hit.identity);
     return `<span class="blatIdPct" style="color:${c}">${hit.identity.toFixed(1)}%</span>`;
 }
 
+function blatUnit() {
+    // A protein query is measured in amino acids, everything else in bases.
+    return hgBlatData.config.isProt ? 'aa' : 'bp';
+}
+
 function blatCoverageCell(hit) {
     var left = (hit.qStart - 1) / hit.qSize * 100;
     var width = (hit.qEnd - hit.qStart + 1) / hit.qSize * 100;
-    var tip = `Query matches the genome at ${blatFmt(hit.qStart)}-${blatFmt(hit.qEnd)}bp out of ${blatFmt(hit.qSize)}bp`;
+    var u = blatUnit();
+    var tip = `Query matches the genome at ${blatFmt(hit.qStart)}-${blatFmt(hit.qEnd)}${u} out of ${blatFmt(hit.qSize)}${u}`;
     return `<span class="blatCov" title="${tip}"><i style="left:${left.toFixed(1)}%;` +
         `width:${width.toFixed(1)}%"></i></span>`;
 }
 
 // ---- summary strip + detail panel ---------------------------------------
 
 function blatSummaryStrip(cfg, queryCount) {
     var stat = (k, v) => `<div class="gbStat"><span class="k">${k}</span>` +
         `<span class="v">${v}</span></div>`;
     var div = '<span class="gbDiv"></span>';
     var assembly = stat('Assembly', htmlEncode(cfg.organism) + ' / ' + htmlEncode(cfg.db)) + div +
         stat('Matches', blatFmt(cfg.hitCount));
     var stats;
     if (cfg.multiQuery) {
         // With more than one query sequence a single query name/length would be wrong, so show the
         // number of distinct queries; each hit's own query is in the table's Query column.
         stats = stat('Queries', blatFmt(queryCount)) + div + assembly;
     } else {
         stats = stat('Query', htmlEncode(cfg.queryName)) + div +
-            stat('Length', blatFmt(cfg.querySize) + ' bp') + div + assembly;
+            stat('Length', blatFmt(cfg.querySize) + ' ' + blatUnit()) + div + assembly;
     }
     var actions = '';
     // "View all in browser" is the primary action, so it comes first.
     if (cfg.viewAllUrl) {
         actions += `<a class="gbPill" title="Open the Genome Browser with all these BLAT hits shown together as one custom track" href="${htmlEncode(cfg.viewAllUrl)}">View all in browser</a>`;
     }
     // "Show Query Sequence" opens the query FASTA in a panel (with Download / Copy). Only on a fresh
     // search, where the uploaded sequence is available (cfg.querySeqs emitted by hgBlat.c).
     if (cfg.querySeqs && cfg.querySeqs.length) {
         actions += '<button type="button" class="gbPill" id="blatSeqBtn" ' +
             'title="Show the sequence you searched with, in FASTA format">Show Query Sequence</button>';
     }
     // "Share a link" just reveals the page's stable URL (cfg.shareUrl, a trash-backed reopen link).
     // cfg.canShare covers old session-based links (?u=&s=), where the current URL is already shareable.
     if (cfg.shareUrl || cfg.canShare) {