1db5c89d97fdd3c3580c4ba9b18eba6b5029c97d
max
  Thu Sep 17 05:09:56 2026 -0700
hgSearch: highlight MANE transcript(s) in RefSeq protein-position search results, refs #38285

#Preview2 week - bugs introduced now will need a build patch to fix
A gene-symbol + codon-range search (e.g. "BRCA1 100-200") maps to every
RefSeq isoform prediction sharing that genomic footprint, which can be
dozens of near-identical NP_ accessions with no indication of which one
is the clinically-relevant MANE transcript. Cross-reference the mane
bigGenePred track by genomic overlap + protein accession and call out
the MANE Select/Plus Clinical transcript(s) in their own section above
the rest, gated behind showManeInSearch (default off) in hg.conf.

diff --git src/hg/lib/bigBedFind.c src/hg/lib/bigBedFind.c
index 70f19589be0..3e195b0d85f 100644
--- src/hg/lib/bigBedFind.c
+++ src/hg/lib/bigBedFind.c
@@ -1,481 +1,582 @@
 #include "common.h"
 #include "bPlusTree.h"
 #include "bbiFile.h"
 #include "bigBed.h"
 #include "hgFind.h"
 #include "trix.h"
 #include "trackHub.h"
 #include "hubConnect.h"
 #include "hdb.h"
 #include "errCatch.h"
 #include "bigBedLabel.h"
 #include "bigBedFind.h"
 #include "genbank.h"
 #include "quickLift.h"
 
 static struct hgPos *bigBedIntervalListToHgPositions(struct cart *cart, struct trackDb *tdb,
                         struct bbiFile *bbi, char *term, struct bigBedInterval *intervalList,
                         char *description, struct hgFindSpec *hfs, char *db)
 /* Given an open bigBed file, and an interval list, return a pointer to a list of hgPos structures. */
 {
 struct hgPos *posList = NULL;
 char chromName[bbi->chromBpt->keySize+1];
 int lastChromId = -1;
 struct bigBedInterval *interval;
 struct slInt *labelColumns = NULL;
 
 // If this track is being quickLifted, the bigBed returns hits in the source
 // assembly's coordinates; we need to remap them to the destination assembly.
 char *quickLiftDb = trackDbSetting(tdb, "quickLiftDb");
 boolean quickLifted = (trackDbSetting(tdb, "quickLiftUrl") != NULL) && isNotEmpty(quickLiftDb);
 char *destDb = trackHubSkipHubName(db);
 
 // Generic searchItemLabel support: allows ${fieldName} patterns in hgFindSpec
 char *searchItemLabel = NULL;
 char **fieldNames = NULL;
 if (hfs)
     searchItemLabel = hgFindSpecSetting(hfs, "searchItemLabel");
 if (searchItemLabel)
     {
     AllocArray(fieldNames, bbi->fieldCount);
     struct slName *field = NULL, *fields = bbFieldNames(bbi);
     int i = 0;
     for (field = fields; field != NULL && i < bbi->fieldCount; field = field->next)
         fieldNames[i++] = field->name;
     }
 
 // MANE/HGNC description support
 struct asObject *as = NULL;
 int ncbiIdIx = -1, geneNameIx = -1;
 struct sqlConnection *conn = NULL;
 if (hfs && (sameString(hfs->searchName, "mane") || sameString(hfs->searchName, "hgnc")))
     {
     as = bigBedAsOrDefault(bbi);
     if (sameString(hfs->searchName, "mane"))
         {
         conn = hAllocConn(db);
         ncbiIdIx = asColumnFindIx(as->columnList, "ncbiId");
         }
     else if (sameString(hfs->searchName, "hgnc"))
         geneNameIx = asColumnFindIx(as->columnList, "geneName");
     }
 
 bigBedLabelCalculateFields(cart, tdb, bbi,  &labelColumns );
 char startBuf[256], endBuf[256], *row[bbi->fieldCount];
 for (interval = intervalList; interval != NULL; interval = interval->next)
     {
     bbiCachedChromLookup(bbi, interval->chromId, lastChromId, chromName, sizeof(chromName));
     lastChromId = interval->chromId;
 
     char *posChrom = chromName;
     int posStart = interval->start;
     int posEnd = interval->end;
     if (quickLifted)
         {
         // Remap source-assembly coords to destination-assembly coords; skip if unmappable.
         if (!quickLiftLiftPos(quickLiftDb, destDb, chromName, interval->start, interval->end,
                               &posChrom, &posStart, &posEnd))
             continue;
         }
 
     struct hgPos *hgPos;
     AllocVar(hgPos);
     slAddHead(&posList, hgPos);
 
     hgPos->chrom = cloneString(posChrom);
     hgPos->chromStart = posStart;
     hgPos->chromEnd = posEnd;
     hgPos->description = cloneString(description);
 
     int rowFieldCount = bigBedIntervalToRow(interval, chromName, startBuf, endBuf, row, bbi->fieldCount);
     if (searchItemLabel)
         {
         hgPos->name = replaceFieldInPattern(searchItemLabel, bbi->fieldCount, fieldNames, row);
         }
     else
         {
         hgPos->name = bigBedMakeLabel(tdb, labelColumns, interval, chromName);
         }
     // browserName needs to correspond to tg->mapItemName()
     if (rowFieldCount > 3)
         hgPos->browserName = cloneString(row[3]);
     else
         hgPos->browserName = cloneString(term);
 
     if (hfs)
         {
         char *paddingStr = hgFindSpecSetting(hfs, "padding");
         int padding = isEmpty(paddingStr) ? 0 : atoi(paddingStr);
         if (padding > 0)
             {
             // highlight the item bases only, to distinguish from padding
             hgPos->highlight = addHighlight(cartString(cart, "db"),
                                             hgPos->chrom, hgPos->chromStart, hgPos->chromEnd);
             hgPos->chromStart -= padding;
             hgPos->chromEnd   += padding;
             if (hgPos->chromStart < 0)
                 hgPos->chromStart = 0;
             }
         // MANE/HGNC description logic
         if (sameString(hfs->searchName, "mane"))
             {
             // here the description comes from hgFixed.refLink.product, linked via mane.bb.ncbiId
             if (ncbiIdIx > 0)
                 {
                 struct dyString *query = sqlDyStringCreate("select product from %s where mrnaAcc=substring_index('%s', '.', 1)", refLinkTable, row[ncbiIdIx]);
                 hgPos->description = sqlQuickString(conn, dyStringCannibalize(&query));
                 }
             }
         else if (sameString(hfs->searchName, "hgnc"))
             {
             // the description is the geneName field of the bigBed
             if (geneNameIx > 0)
                 hgPos->description = cloneString(row[geneNameIx]);
             }
         }
     }
 
 if (conn)
     hFreeConn(&conn);
 
 return posList;
 }
 
 static struct hgPos *getPosFromBigBed(struct cart *cart, struct trackDb *tdb, struct bbiFile *bbi,
                         char *indexField, char *term, char *description, struct hgFindSpec *hfs, char *db)
 /* Given a bigBed file with a search index, check for term. */
 {
 struct errCatch *errCatch = errCatchNew();
 struct hgPos *posList = NULL;
 if (errCatchStart(errCatch))
     {
     int fieldIx;
     struct bptFile *bpt = bigBedOpenExtraIndex(bbi, indexField, &fieldIx);
     struct lm *lm = lmInit(0);
     struct bigBedInterval *intervalList;
     intervalList = bigBedNameQuery(bbi, bpt, fieldIx, term, lm);
 
     posList = bigBedIntervalListToHgPositions(cart, tdb,  bbi, term, intervalList, description, hfs, db);
     bptFileDetach(&bpt);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError) 
     {
     // we fail silently if there is a problem e.g. bad index name
     return NULL;
     }
 
 return posList;
 }
 
 static struct hgPos *doTrixSearch(struct cart *cart, struct trackDb *tdb, char *trixFile,
                         struct slName *indices, struct bbiFile *bbi, char *term, char *description,
                         struct hgFindSpec *hfs, char *db)
 /* search a trix file in the "searchTrix" field of a bigBed trackDb */
 {
 struct trix *trix = trixOpen(trixFile);
 int trixWordCount = 0;
 char *tmp = cloneString(term);
 char *val = nextWord(&tmp);
 char *trixWords[128];
 
 while (val != NULL)
     {
     trixWords[trixWordCount] = strLower(val);
     trixWordCount++;
     if (trixWordCount == sizeof(trixWords)/sizeof(char*))
 	errAbort("exhausted space for trixWords");
 
     val = nextWord(&tmp);        
     }
 
 if (trixWordCount == 0)
     return NULL;
 
 
 struct trixSearchResult *tsList = trixSearch(trix, trixWordCount, trixWords, tsmExpand);
 char *context = NULL;
 if (hfs)
     context = hgFindSpecSetting(hfs, "searchTrixContext");
 boolean doSnippets = FALSE;
 if (context && sameString(context, "on"))
     {
     doSnippets = TRUE;
     initSnippetIndex(trix);
     }
 struct hgPos *posList = NULL;
 for ( ; tsList != NULL; tsList = tsList->next)
     {
     struct slName *oneIndex = indices;
     if (doSnippets)
         addSnippetForResult(tsList, trix);
     for (; oneIndex; oneIndex = oneIndex->next)
 	{
 	struct hgPos *posList2 = getPosFromBigBed(cart, tdb, bbi, oneIndex->name,
                                                   tsList->itemId, tsList->snippet, hfs, db);
 
 	posList = slCat(posList, posList2);
 	}
     }
 
 return posList;
 }
 
 int posListCompare(const void *va, const void *vb)
 /* Compare to sort based on name and then position. */
 {
 const struct hgPos *a = *((struct hgPos **)va);
 const struct hgPos *b = *((struct hgPos **)vb);
 int diff = strcmp(a->name, b->name);
 if (diff == 0)
     {
     diff = strcmp(a->chrom, b->chrom);
     if (diff == 0)
         {
         diff = a->chromStart - b->chromStart;
         if (diff == 0)
             diff = a->chromEnd - b->chromEnd;
         }
     }
 return diff;
 }
 
 
 
 boolean findBigBedPosInTdb(struct cart *cart, char *db, struct trackDb *tdb, char *term, struct hgPositions *hgp, struct hgFindSpec *hfs, boolean measureTiming)
 /* Find a position in a single trackDb entry */
 {
 if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type))
     return FALSE;
 long startTime = clock1000();
 boolean found = FALSE;
 char *description = NULL;
 
 if (hfs)
     {
     char buf[2048];
     if (isNotEmpty(hfs->searchDescription))
         truncatef(buf, sizeof(buf), "%s", hfs->searchDescription);
     else
         safef(buf, sizeof(buf), "%s", hfs->searchTable);
     description = cloneString(buf);
     }
 
 // Which field(s) to search?  Look for searchIndex in search spec, then in trackDb for
 // backwards compat.
 char *indexField = NULL;
 if (hfs)
     indexField = hgFindSpecSetting(hfs, "searchIndex");
 if (!indexField)
     indexField = trackDbSetting(tdb, "searchIndex");
 if (!indexField && !hfs)
     return FALSE;
 
 // If !indexField but we do have a non-NULL hfs, then open file to see if it has a name index.
 char *fileName = trackDbSetting(tdb, "bigDataUrl");
 if (!fileName && !trackHubDatabase(db))
     {
     struct sqlConnection *conn = hAllocConnTrack(db, tdb);
     fileName = bbiNameFromSettingOrTable(tdb, conn, tdb->table);
     hFreeConn(&conn);
     }
 if (!fileName)
     return FALSE;
 // we fail silently if bigBed can't be opened.
 struct bbiFile *bbi = NULL;
 struct errCatch *errCatch = errCatchNew();
 if (errCatchStart(errCatch))
     {
     bbi = bigBedFileOpen(fileName);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     return FALSE;
 
 // Now (since hfs is non-NULL) check the file to see if it has a name index if we
 // don't already have indexField.
 if (!indexField)
     {
     struct slName *indexFields = bigBedListExtraIndexes(bbi);
     if (slNameInList(indexFields, "name"))
         indexField = "name";
     slNameFreeList(&indexFields);
     }
 if (!indexField)
     {
     bigBedFileClose(&bbi);
     return FALSE;
     }
 
 struct slName *indexList = slNameListFromString(indexField, ',');
 struct hgPos *posList1 = NULL, *posList2 = NULL;
 char *trixFile = trackDbSetting(tdb, "searchTrix");
 // if there is a trix file, use it to search for the term
 if (trixFile != NULL)
     {
     struct errCatch *errCatch = errCatchNew();
     if (errCatchStart(errCatch))
         {
         posList1 = doTrixSearch(cart, tdb, hReplaceGbdb(trixFile), indexList, bbi, term,
                                 NULL, hfs, db);
         }
     errCatchEnd(errCatch);
     if (errCatch->gotError)
         warn("trix search failure for %s: %s", tdb->table, dyStringContents(errCatch->message));
     errCatchFree(&errCatch);
     }
 
 // now search for the raw id's
 struct slName *oneIndex=indexList;
 for (; oneIndex; oneIndex = oneIndex->next)
     {
     posList2 = getPosFromBigBed(cart, tdb, bbi, oneIndex->name, term, NULL, hfs, db);
     posList1 = slCat(posList1, posList2);
     }
 // the trix search and the id search may have found the same item so uniqify:
 slUniqify(&posList1, posListCompare, hgPosFree);
 
 if (posList1 != NULL)
     {
     struct hgPosTable *table;
 
     found = TRUE;
     AllocVar(table);
     slAddHead(&hgp->tableList, table);
     table->description = cloneString(description ? description : tdb->longLabel);
     table->name = cloneString(tdb->table);
     table->searchTime = -1;
         if (measureTiming)
             table->searchTime = clock1000() - startTime;
 
     table->posList = posList1;
     }
 bigBedFileClose(&bbi);
 freeMem(description);
 return found;
 }
 
 boolean findBigBedPosInTdbList(struct cart *cart, char *db, struct trackDb *tdbList, char *term, struct hgPositions *hgp, struct hgFindSpec *hfs, boolean measureTiming)
 /* Given a list of trackDb entries, check each of them for a searchIndex */
 {
 struct trackDb *tdb;
 boolean found = FALSE;
 for (tdb = tdbList; tdb; tdb = tdb->next)
     {
     if (tdb->subtracks)
         {
         found = findBigBedPosInTdbList(cart, db, tdb->subtracks, term, hgp, hfs, measureTiming) || found;
         continue;
         }
     found = findBigBedPosInTdb(cart, db, tdb, term, hgp, hfs, measureTiming) || found;
     }
 return found;
 }
 
 boolean isTdbSearchable(struct trackDb *tdb)
 /* Check if a single tdb is searchable */
 {
 if (tdb->subtracks)
     {
     boolean searchable = FALSE;
     struct trackDb *sub;
     for (sub = tdb->subtracks; sub != NULL; sub = sub->next)
         searchable |= isTdbSearchable(sub);
     return searchable;
     }
 if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type))
     return FALSE;
 
 char *indexField = NULL;
 indexField = trackDbSetting(tdb, "searchIndex");
 if (indexField)
     return TRUE;
 
 // If !indexField but we do have an index on the bigBed use that
 char *fileName = trackDbSetting(tdb, "bigDataUrl");
 if (!fileName)
     return FALSE;
 
 // we fail silently if bigBed can't be opened.
 struct bbiFile *bbi = NULL;
 struct errCatch *errCatch = errCatchNew();
 if (errCatchStart(errCatch))
     {
     bbi = bigBedFileOpen(fileName);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     return FALSE;
 
 if (!indexField)
     {
     struct slName *indexFields = bigBedListExtraIndexes(bbi);
     if (slNameInList(indexFields, "name"))
         indexField = "name";
     slNameFreeList(&indexFields);
     }
 if (!indexField)
     {
     bigBedFileClose(&bbi);
     return FALSE;
     }
 return TRUE;
 }
 
 struct trackDb *getSearchableBigBeds(struct trackDb *tdbList)
 /* Given a list of tracks from a hub, return those that are searchable */
 {
 struct trackDb *tdb, *next, *ret = NULL;
 for (tdb = tdbList ; tdb; tdb = next)
     {
     next = tdb->next;
     if (tdb->subtracks)
         {
         struct trackDb *subtrackList = getSearchableBigBeds(tdb->subtracks);
         if (subtrackList)
             ret = slCat(subtrackList, ret);
         continue;
         }
     if (startsWith("bigWig", tdb->type) || !startsWith("big", tdb->type))
         continue;
 
     char *indexField = NULL;
     indexField = trackDbSetting(tdb, "searchIndex");
     if (!indexField)
         continue;
 
     // If !indexField but we do have an index on the bigBed use that
     char *fileName = trackDbSetting(tdb, "bigDataUrl");
     if (!fileName)
         continue;
 
     // we fail silently if bigBed can't be opened.
     struct bbiFile *bbi = NULL;
     struct errCatch *errCatch = errCatchNew();
     if (errCatchStart(errCatch))
         {
         bbi = bigBedFileOpen(fileName);
         }
     errCatchEnd(errCatch);
     if (errCatch->gotError)
         continue;
 
     if (!indexField)
         {
         struct slName *indexFields = bigBedListExtraIndexes(bbi);
         if (slNameInList(indexFields, "name"))
             indexField = "name";
         slNameFreeList(&indexFields);
         }
     if (!indexField)
         {
         bigBedFileClose(&bbi);
         continue;
         }
 
     // finally we have verified the track is searchable, add it to our list we're returning
     // careful not to mess up the old list
     slAddHead(&ret, CloneVar(tdb));
     }
 return ret;
 }
+
+struct maneLookup
+/* An open handle on the "mane" bigGenePred track, for repeated region lookups. */
+    {
+    struct bbiFile *bbi;
+    int protAccIx;
+    int maneStatIx;
+    };
+
+struct maneLookup *maneLookupOpen(char *db, struct trackDb **tdbList)
+/* Open the "mane" bigGenePred track for db, if it exists, for repeated calls to
+ * maneStatusForRegion.  Returns NULL if this assembly has no mane track (e.g. non-human,
+ * or hg19) -- callers should treat that as "can't tell", not an error.  tdbList is passed
+ * through to tdbForTrack, so pass a pointer already shared with other tdbForTrack calls in
+ * the same request to avoid a second trackDb load. */
+{
+struct trackDb *tdb = tdbForTrack(db, "mane", tdbList);
+if (tdb == NULL)
+    return NULL;
+char *fileName = trackDbSetting(tdb, "bigDataUrl");
+if (isEmpty(fileName))
+    return NULL;
+
+struct maneLookup *ml = NULL;
+struct errCatch *errCatch = errCatchNew();
+if (errCatchStart(errCatch))
+    {
+    struct bbiFile *bbi = bigBedFileOpen(hReplaceGbdb(fileName));
+    struct asObject *as = bigBedAsOrDefault(bbi);
+    AllocVar(ml);
+    ml->bbi = bbi;
+    ml->protAccIx = asColumnFindIx(as->columnList, "ncbiProtAcc");
+    ml->maneStatIx = asColumnFindIx(as->columnList, "maneStat");
+    }
+errCatchEnd(errCatch);
+if (errCatch->gotError)
+    ml = NULL;
+errCatchFree(&errCatch);
+return ml;
+}
+
+static boolean protAccMatches(char *rowProtAcc, struct slName *protAccList)
+/* Return TRUE if rowProtAcc matches one of protAccList, ignoring version (".N") suffixes. */
+{
+char *rowDot = strchr(rowProtAcc, '.');
+int rowLen = rowDot ? (rowDot - rowProtAcc) : (int)strlen(rowProtAcc);
+struct slName *pa;
+for (pa = protAccList; pa != NULL; pa = pa->next)
+    {
+    char *paDot = strchr(pa->name, '.');
+    int paLen = paDot ? (paDot - pa->name) : (int)strlen(pa->name);
+    if (rowLen > 0 && rowLen == paLen && memcmp(rowProtAcc, pa->name, rowLen) == 0)
+        return TRUE;
+    }
+return FALSE;
+}
+
+char *maneStatusForRegion(struct maneLookup *ml, char *chrom, int start, int end,
+                          struct slName *protAccList, char **retProtAcc)
+/* Look for a MANE transcript overlapping chrom:start-end whose NCBI protein accession
+ * (ignoring version suffix) matches one of protAccList.  Returns a cloned "MANE Select" /
+ * "MANE Plus Clinical" string, or NULL if ml is NULL or there is no match.  On a match,
+ * *retProtAcc is set to a cloned copy of the matching (versioned) NCBI protein accession,
+ * so callers can identify the single MANE transcript out of a group of merged accessions. */
+{
+if (ml == NULL || ml->protAccIx < 0 || ml->maneStatIx < 0)
+    return NULL;
+char *result = NULL;
+struct lm *lm = lmInit(0);
+struct errCatch *errCatch = errCatchNew();
+if (errCatchStart(errCatch))
+    {
+    struct bigBedInterval *intervalList = bigBedIntervalQuery(ml->bbi, chrom, start, end, 0, lm);
+    struct bigBedInterval *interval;
+    char startBuf[16], endBuf[16], *row[ml->bbi->fieldCount];
+    for (interval = intervalList; interval != NULL && result == NULL; interval = interval->next)
+        {
+        bigBedIntervalToRow(interval, chrom, startBuf, endBuf, row, ml->bbi->fieldCount);
+        if (protAccMatches(row[ml->protAccIx], protAccList))
+            {
+            result = cloneString(row[ml->maneStatIx]);
+            if (retProtAcc != NULL)
+                *retProtAcc = cloneString(row[ml->protAccIx]);
+            }
+        }
+    }
+errCatchEnd(errCatch);
+errCatchFree(&errCatch);
+lmCleanup(&lm);
+return result;
+}
+
+void maneLookupClose(struct maneLookup **pMl)
+/* Close a maneLookup opened by maneLookupOpen. */
+{
+if (pMl != NULL && *pMl != NULL)
+    {
+    bigBedFileClose(&(*pMl)->bbi);
+    freez(pMl);
+    }
+}