diff --git a/util/coverage/coverage.d.ts b/util/coverage/coverage.d.ts
index 42a9e5133e..d6ad2ccc10 100644
--- a/util/coverage/coverage.d.ts
+++ b/util/coverage/coverage.d.ts
@@ -25,6 +25,7 @@ export type CoverageEntry = {
     name: string;
     commit?: string;
     tag?: string;
+    tagHash?: string;
   }[];
   lastModified: number;
 };
@@ -61,6 +62,10 @@ export type Tags = {
   [tagName: string]: {
     tagDate: number;
     tagHash: string;
+    files: {
+      name: string;
+      hash: string;
+    }[];
   };
 };
 
@@ -69,4 +74,5 @@ export type Pulls = {
   number: number;
   title: string;
   files: string[];
+  zones: number[];
 }[];
diff --git a/util/coverage/coverage.ts b/util/coverage/coverage.ts
index f438ca282b..d4c5bcc318 100644
--- a/util/coverage/coverage.ts
+++ b/util/coverage/coverage.ts
@@ -490,7 +490,14 @@ const buildZoneGrid = (container: HTMLElement, lang: Lang, coverage: Coverage) =
         const hasOopsy = zoneCoverage.oopsy && zoneCoverage.oopsy.num > 0;
         const hasTriggers = zoneCoverage.triggers.num > 0;
 
-        if (!hasTriggers && !hasOopsy && !zoneCoverage.timeline.hasFile) {
+        const openPRs = pulls
+          .filter((pr) =>
+            (pr.files.find((file) => zoneCoverage.files.find((file2) => file === file2.name)) !==
+              undefined) ||
+            pr.zones.includes(zoneId)
+          );
+
+        if (!hasTriggers && !hasOopsy && !zoneCoverage.timeline.hasFile && openPRs.length === 0) {
           const div = addDiv(container, 'text', translate(miscStrings.unsupported, lang));
           div.style.color = 'red';
           return;
@@ -516,14 +523,10 @@ const buildZoneGrid = (container: HTMLElement, lang: Lang, coverage: Coverage) =
           ),
         ].sort((left, right) => right?.tagDate - left?.tagDate)[0];
 
-        const openPRs = pulls
-          .filter((pr) =>
-            pr.files.find((file) => zoneCoverage.files.find((file2) => file === file2.name))
-          );
-
         let color = 'green';
 
-        const unreleased = version?.tag === undefined;
+        const unreleased = version?.tag === undefined ||
+          (version?.tagDate ?? 0) < zoneCoverage.lastModified;
 
         if (unreleased || openPRs.length > 0) {
           color = 'orange';
@@ -535,7 +538,7 @@ const buildZoneGrid = (container: HTMLElement, lang: Lang, coverage: Coverage) =
           lastUpdated.toString();
 
         if (openPRs.length > 0) {
-          titleText += `Open PRs: ${openPRs.map((pr) => `#${pr.number}`).join(', ')} | `;
+          titleText = `Open PRs: ${openPRs.map((pr) => `#${pr.number}`).join(', ')} | ${titleText}`;
         }
 
         const div = document.createElement('div');
diff --git a/util/gen_coverage_report.ts b/util/gen_coverage_report.ts
index 9807690a40..9691c9afe9 100644
--- a/util/gen_coverage_report.ts
+++ b/util/gen_coverage_report.ts
@@ -26,19 +26,6 @@ import {
 import { findMissingTranslations, MissingTranslationErrorType } from './find_missing_translations';
 import findManifestFiles from './manifest';
 
-// eslint can't find these types from simple-git for some reason
-// Redefine the parts that we actually use
-type TagResult = {
-  all: string[];
-};
-
-type LogResult = {
-  latest: {
-    date: string;
-    hash: string;
-  } | null;
-};
-
 type MissingTranslations = {
   file: string;
   line?: number;
@@ -50,6 +37,17 @@ type MissingTranslationsDict = {
   [lang in Lang]?: MissingTranslations[];
 };
 
+type SimpleGit = ReturnType<typeof simpleGit>;
+
+// This hash is the default "initial commit" hash for git, all repos have it
+// If for some reason the entire git tree is re-imported into a newer version of git,
+// the default commit hash will be an SHA-256 hash as follows instead:
+// 6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321
+// This can be derived as needed via `git hash-object -t tree /dev/null`
+const DEFAULT_COMMIT_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
+
+const notUndefined = <T>(v: T | undefined): v is T => v !== undefined;
+
 // Paths are relative to current file.
 // We can't import the manifest directly from util/ because that's webpack magic,
 // so need to do the same processing its loader would do.
@@ -482,20 +480,48 @@ const writeMissingTranslations = (missing: MissingTranslations[], outputFileName
   }
 };
 
-(async () => {
-  const git = simpleGit();
+const mapCoverageTags = async (coverage: Coverage, git: SimpleGit, tags: Tags) => {
+  const reverseOrderTags = Object.keys(tags).reverse();
+
+  for (const coverageEntry of Object.values(coverage)) {
+    for (const file of coverageEntry.files) {
+      const logData = await git.log({
+        file: file.name,
+        maxCount: 1,
+      });
+
+      if (logData === undefined)
+        continue;
 
-  let tagData: TagResult | undefined;
+      const latest = logData.latest;
+      if (latest !== null) {
+        coverageEntry.lastModified = Math.max(
+          coverageEntry.lastModified,
+          (new Date(latest.date)).getTime(),
+        );
+        file.commit = latest.hash;
+      }
 
-  await git.tags({
-    '--sort': '-authordate',
+      if (file.commit !== undefined) {
+        for (const tag of reverseOrderTags) {
+          const tagFile = tags[tag]?.files.find((tagFile) => tagFile.name === file.name);
+          if (tagFile) {
+            file.tag = tag;
+            file.tagHash = tagFile.hash;
+            break;
+          }
+        }
+      }
+    }
+  }
+};
+
+const extractTagsAndPulls = async (git: SimpleGit) => {
+  const tagData = await git.tags({
     '--format': '%(objectname)|%(refname:strip=2)|%(authordate)|%(*authordate)',
-  }, (_err, data) => {
-    tagData = data;
   });
-  tagData ??= undefined;
 
-  const tags: Tags = {};
+  const unsortedTags: (Tags[string] & { name: string })[] = [];
 
   for (const tag of tagData?.all ?? []) {
     const [tagHash, tagName, tagDate, commitDate] = tag.split('|', 4);
@@ -508,9 +534,35 @@ const writeMissingTranslations = (missing: MissingTranslations[], outputFileName
     if (isNaN(tagDateObj.getTime())) {
       tagDateObj = new Date(commitDate);
     }
-    tags[tagName] = {
+    unsortedTags.push({
+      name: tagName,
       tagDate: tagDateObj.getTime(),
       tagHash: tagHash,
+      files: [],
+    });
+  }
+
+  const tags: Tags = {};
+
+  let lastVersion = DEFAULT_COMMIT_HASH;
+
+  for (const tag of unsortedTags.sort((l, r) => l.tagDate - r.tagDate)) {
+    const result = await git.raw(['diff-tree', '-r', lastVersion, tag.name]);
+    lastVersion = tag.name;
+
+    tags[tag.name] = {
+      tagDate: tag.tagDate,
+      tagHash: tag.tagHash,
+      files: result.split('\n').map((line) => {
+        const matches =
+          /^:(?<oldMode>[^\s]+) (?<newMode>[^\s]+) (?<oldHash>[^\s]+) (?<newHash>[^\s]+) (?<action>[^\s]+)\s*(?<name>[^\s].*?)$/
+            .exec(line);
+
+        return {
+          hash: matches?.groups?.['newHash'] ?? '',
+          name: matches?.groups?.['name'] ?? '',
+        };
+      }),
     };
   }
 
@@ -537,13 +589,33 @@ const writeMissingTranslations = (missing: MissingTranslations[], outputFileName
 
     const files = pullFiles.map((f) => f.filename);
 
+    const zones = pullFiles
+      .filter((f) =>
+        (f.filename.startsWith('ui/raidboss/data') ||
+          f.filename.startsWith('ui/oopsyraidsy/data')) && f.filename.endsWith('.ts')
+      )
+      .map((f) => /ZoneId\.([a-zA-Z0-9]+)/.exec(f.patch ?? '')?.[1])
+      .filter(notUndefined)
+      .map((zoneId) =>
+        zoneId in ZoneId ? ZoneId[zoneId as keyof typeof ZoneId] as number : undefined
+      )
+      .filter(notUndefined);
+
     pulls.push({
       number: openPull.number,
       title: openPull.title,
       url: openPull.html_url,
       files: files,
+      zones: zones,
     });
   }
+  return { tags, pulls };
+};
+
+(async () => {
+  const git = simpleGit();
+
+  const { tags, pulls }: { tags: Tags; pulls: Pulls } = await extractTagsAndPulls(git);
 
   // Do this prior to chdir which conflicts with find_missing_timeline_translations.ts.
   // FIXME: make that script more robust to cwd.
@@ -564,42 +636,7 @@ const writeMissingTranslations = (missing: MissingTranslations[], outputFileName
   await processRaidbossCoverage(raidbossManifest, coverage, missingTranslations);
   await processOopsyCoverage(oopsyManifest, coverage);
 
-  for (const coverageEntry of Object.values(coverage)) {
-    for (const file of coverageEntry.files) {
-      let logData: undefined | LogResult;
-      await git.log({
-        file: file.name,
-        maxCount: 1,
-      }, (_err, data) => logData = data);
-
-      logData ??= undefined;
-
-      if (logData === undefined)
-        continue;
-
-      const latest = logData.latest;
-      if (latest !== null) {
-        coverageEntry.lastModified = Math.max(
-          coverageEntry.lastModified,
-          (new Date(latest.date)).getTime(),
-        );
-        file.commit = latest.hash;
-      }
-
-      if (file.commit !== undefined) {
-        let tagData: undefined | string;
-        await git.tag({
-          '--sort': 'authordate',
-          '--contains': file.commit,
-        }, (_err, data) => tagData = data.split('\n')[0]?.trim());
-        tagData ??= undefined;
-
-        if (tagData !== undefined) {
-          file.tag = tagData;
-        }
-      }
-    }
-  }
+  await mapCoverageTags(coverage, git, tags);
 
   const { totals, translationTotals } = buildTotals(coverage, missingTranslations);
   writeCoverageReport(