OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      64a3e04ec7f2
DATE      2026-07-17
SUBJECT   web: latin-only fonts, drop unused 500, replace fuse.js with a 40-line
          scorer
FILES     6 CHANGED
HASH      64a3e04ec7f2
DATE      2026-07-17
SUBJECT   web: latin-only fonts, drop unused
          500, replace fuse.js with a 40-line
          scorer
FILES     6 CHANGED
 

diff --git a/ooknet-design/package-lock.json b/ooknet-design/package-lock.json
index 3482424..d17bd9b 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -10,8 +10,7 @@
       "dependencies": {
         "@fontsource/cascadia-mono": "^5.2.3",
         "@fontsource/ibm-plex-mono": "^5.2.7",
-        "astro": "^6.1.9",
-        "fuse.js": "^7.3.0"
+        "astro": "^6.1.9"
       },
       "devDependencies": {
         "sass": "^1.99.0",
@@ -2895,19 +2894,6 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
-    "node_modules/fuse.js": {
-      "version": "7.3.0",
-      "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.3.0.tgz",
-      "integrity": "sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKF
THbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/krisk"
-      }
-    },
     "node_modules/github-slugger": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0
.0.tgz",

diff --git a/ooknet-design/package-lock.json
 b/ooknet-design/package-lock.json
index 3482424..d17bd9b 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -10,8 +10,7 @@
       "dependencies": {
         "@fontsource/cascadia-mono": "^5.2.
3",
         "@fontsource/ibm-plex-mono": "^5.2.
7",
-        "astro": "^6.1.9",
-        "fuse.js": "^7.3.0"
+        "astro": "^6.1.9"
       },
       "devDependencies": {
         "sass": "^1.99.0",
@@ -2895,19 +2894,6 @@
         "node": "^8.16.0 || ^10.6.0 || >=11
.0.0"
       }
     },
-    "node_modules/fuse.js": {
-      "version": "7.3.0",
-      "resolved": "https://registry.npmjs.o
rg/fuse.js/-/fuse.js-7.3.0.tgz",
-      "integrity": "sha512-plz8RVjfcDedTGfV
ngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoC
iYyzaK2Ws8lABkTCcRSqCY1q4w==",
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors
/krisk"
-      }
-    },
     "node_modules/github-slugger": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.o
rg/github-slugger/-/github-slugger-2.0.0.tgz
",
 

diff --git a/ooknet-design/package.json b/ooknet-design/package.json
index 78b118d..31126d6 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -15,8 +15,7 @@
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
     "@fontsource/ibm-plex-mono": "^5.2.7",
-    "astro": "^6.1.9",
-    "fuse.js": "^7.3.0"
+    "astro": "^6.1.9"
   },
   "devDependencies": {
     "sass": "^1.99.0",

diff --git a/ooknet-design/package.json b/oo
knet-design/package.json
index 78b118d..31126d6 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -15,8 +15,7 @@
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
     "@fontsource/ibm-plex-mono": "^5.2.7",
-    "astro": "^6.1.9",
-    "fuse.js": "^7.3.0"
+    "astro": "^6.1.9"
   },
   "devDependencies": {
     "sass": "^1.99.0",
 

diff --git a/ooknet-design/src/components/TopHeader/TopHeader.astro b/ooknet-desig
n/src/components/TopHeader/TopHeader.astro
index ae32ca8..6e565d6 100644
--- a/ooknet-design/src/components/TopHeader/TopHeader.astro
+++ b/ooknet-design/src/components/TopHeader/TopHeader.astro
@@ -39,26 +39,14 @@ const PLACEHOLDER_NARROW = "_".repeat(18);
 </div>
 
 <script>
-  import Fuse from "fuse.js";
+  import { searchIndex, type SearchItem } from "../../lib/search";
 
-  type Note = { note: string; title: string; category: string; tags: string[]; fi
led: string; url: string };
-  let fuse: Fuse<Note> | null = null;
+  let data: SearchItem[] | null = null;
 
   async function ensureLoaded() {
-    if (fuse) return;
+    if (data) return;
     const res = await fetch("/search.json");
-    const data: Note[] = await res.json();
-    fuse = new Fuse(data, {
-      keys: [
-        { name: "title", weight: 2 },
-        { name: "note", weight: 1.5 },
-        { name: "category", weight: 1 },
-        { name: "tags", weight: 1 },
-        { name: "filed", weight: 1 },
-      ],
-      threshold: 0.4,
-      ignoreLocation: true,
-    });
+    data = await res.json();
   }
 
   function getResultsList(input: HTMLInputElement) {
@@ -81,14 +69,13 @@ const PLACEHOLDER_NARROW = "_".repeat(18);
     list.innerHTML = "";
   }
 
-  function render(list: HTMLUListElement, hits: { item: Note }[]) {
+  function render(list: HTMLUListElement, hits: SearchItem[]) {
     if (!hits.length) { close(list); return; }
     list.hidden = false;
     list.innerHTML = hits
-      .slice(0, 8)
-      .map((h, i) => {
+      .map((item, i) => {
         const safe = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;"
).replace(/>/g, "&gt;");
-        return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(h.item.url
)}">${safe(h.item.note)} - ${safe(h.item.title)}</a></li>`;
+        return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(item.url)}
">${safe(item.note)} - ${safe(item.title)}</a></li>`;
       })
       .join("");
   }
@@ -101,7 +88,7 @@ const PLACEHOLDER_NARROW = "_".repeat(18);
     const q = input.value.trim();
     if (!q) { close(list); return; }
     await ensureLoaded();
-    render(list, fuse!.search(q));
+    render(list, searchIndex(data!, q));
   }
 
   function onKeydown(e: KeyboardEvent) {

diff --git a/ooknet-design/src/components/To
pHeader/TopHeader.astro b/ooknet-design/src/
components/TopHeader/TopHeader.astro
index ae32ca8..6e565d6 100644
--- a/ooknet-design/src/components/TopHeader
/TopHeader.astro
+++ b/ooknet-design/src/components/TopHeader
/TopHeader.astro
@@ -39,26 +39,14 @@ const PLACEHOLDER_NARROW
 = "_".repeat(18);
 </div>
 
 <script>
-  import Fuse from "fuse.js";
+  import { searchIndex, type SearchItem } f
rom "../../lib/search";
 
-  type Note = { note: string; title: string
; category: string; tags: string[]; filed: s
tring; url: string };
-  let fuse: Fuse<Note> | null = null;
+  let data: SearchItem[] | null = null;
 
   async function ensureLoaded() {
-    if (fuse) return;
+    if (data) return;
     const res = await fetch("/search.json")
;
-    const data: Note[] = await res.json();
-    fuse = new Fuse(data, {
-      keys: [
-        { name: "title", weight: 2 },
-        { name: "note", weight: 1.5 },
-        { name: "category", weight: 1 },
-        { name: "tags", weight: 1 },
-        { name: "filed", weight: 1 },
-      ],
-      threshold: 0.4,
-      ignoreLocation: true,
-    });
+    data = await res.json();
   }
 
   function getResultsList(input: HTMLInputE
lement) {
@@ -81,14 +69,13 @@ const PLACEHOLDER_NARROW
 = "_".repeat(18);
     list.innerHTML = "";
   }
 
-  function render(list: HTMLUListElement, h
its: { item: Note }[]) {
+  function render(list: HTMLUListElement, h
its: SearchItem[]) {
     if (!hits.length) { close(list); return
; }
     list.hidden = false;
     list.innerHTML = hits
-      .slice(0, 8)
-      .map((h, i) => {
+      .map((item, i) => {
         const safe = (s: string) => s.repla
ce(/&/g, "&amp;").replace(/</g, "&lt;").repl
ace(/>/g, "&gt;");
-        return `<li${i === 0 ? ' class="act
ive"' : ""}><a href="${safe(h.item.url)}">${
safe(h.item.note)} - ${safe(h.item.title)}</
a></li>`;
+        return `<li${i === 0 ? ' class="act
ive"' : ""}><a href="${safe(item.url)}">${sa
fe(item.note)} - ${safe(item.title)}</a></li
>`;
       })
       .join("");
   }
@@ -101,7 +88,7 @@ const PLACEHOLDER_NARROW 
= "_".repeat(18);
     const q = input.value.trim();
     if (!q) { close(list); return; }
     await ensureLoaded();
-    render(list, fuse!.search(q));
+    render(list, searchIndex(data!, q));
   }
 
   function onKeydown(e: KeyboardEvent) {
 

diff --git a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro b/ooknet-
design/src/layouts/ArticleLayout/ArticleLayout.astro
index 2314452..b91d047 100644
--- a/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayout/ArticleLayout.astro
@@ -1,11 +1,12 @@
 ---
-import "@fontsource/ibm-plex-mono/400.css";
-import "@fontsource/ibm-plex-mono/500.css";
-import "@fontsource/ibm-plex-mono/600.css";
-import "@fontsource/ibm-plex-mono/700.css";
-import "@fontsource/ibm-plex-mono/400-italic.css";
-import "@fontsource/cascadia-mono/400.css";
-import "@fontsource/cascadia-mono/700.css";
+// latin-only: the full css ships every script's files into dist (~1MB
+// of fonts the site never uses). 500 dropped - nothing sets it.
+import "@fontsource/ibm-plex-mono/latin-400.css";
+import "@fontsource/ibm-plex-mono/latin-600.css";
+import "@fontsource/ibm-plex-mono/latin-700.css";
+import "@fontsource/ibm-plex-mono/latin-400-italic.css";
+import "@fontsource/cascadia-mono/latin-400.css";
+import "@fontsource/cascadia-mono/latin-700.css";
 import "../../styles/global.scss";
 import "./ArticleLayout.scss";
 import Toast from "../../components/Toast/Toast.astro";

diff --git a/ooknet-design/src/layouts/Artic
leLayout/ArticleLayout.astro b/ooknet-design
/src/layouts/ArticleLayout/ArticleLayout.ast
ro
index 2314452..b91d047 100644
--- a/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
+++ b/ooknet-design/src/layouts/ArticleLayou
t/ArticleLayout.astro
@@ -1,11 +1,12 @@
 ---
-import "@fontsource/ibm-plex-mono/400.css";
-import "@fontsource/ibm-plex-mono/500.css";
-import "@fontsource/ibm-plex-mono/600.css";
-import "@fontsource/ibm-plex-mono/700.css";
-import "@fontsource/ibm-plex-mono/400-itali
c.css";
-import "@fontsource/cascadia-mono/400.css";
-import "@fontsource/cascadia-mono/700.css";
+// latin-only: the full css ships every scr
ipt's files into dist (~1MB
+// of fonts the site never uses). 500 dropp
ed - nothing sets it.
+import "@fontsource/ibm-plex-mono/latin-400
.css";
+import "@fontsource/ibm-plex-mono/latin-600
.css";
+import "@fontsource/ibm-plex-mono/latin-700
.css";
+import "@fontsource/ibm-plex-mono/latin-400
-italic.css";
+import "@fontsource/cascadia-mono/latin-400
.css";
+import "@fontsource/cascadia-mono/latin-700
.css";
 import "../../styles/global.scss";
 import "./ArticleLayout.scss";
 import Toast from "../../components/Toast/T
oast.astro";
 

diff --git a/ooknet-design/src/lib/search.test.ts b/ooknet-design/src/lib/search.t
est.ts
new file mode 100644
index 0000000..1275886
--- /dev/null
+++ b/ooknet-design/src/lib/search.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from "vitest";
+import { searchIndex, type SearchItem } from "./search";
+
+const ITEMS: SearchItem[] = [
+  { note: "N217", title: "ENTROPY BOUNDS IN SPARSE GRAPHS", category: "graph-theo
ry", tags: ["entropy", "sparse-graphs"], filed: "/INFO/GRAPH/ENTROPY/BOUND", url: 
"/notes/n217-entropy-bounds/" },
+  { note: "N0", title: "STYLE SHEET", category: "design-system", tags: ["design-s
ystem", "reference"], filed: "/META/STYLE/SHEET", url: "/notes/n0-style-sheet/" },
+  { note: "DOC", title: "Wikilinks", category: "docs", tags: [], filed: "/DOCS/AU
THORING/WIKILINKS", url: "/docs/authoring/wikilinks/" },
+];
+
+describe("searchIndex", () => {
+  it("matches note numbers first", () => {
+    expect(searchIndex(ITEMS, "n217")[0].url).toBe("/notes/n217-entropy-bounds/")
;
+  });
+
+  it("matches title substrings", () => {
+    expect(searchIndex(ITEMS, "entropy")[0].note).toBe("N217");
+  });
+
+  it("matches tags exactly, as tag badges search", () => {
+    expect(searchIndex(ITEMS, "reference")[0].note).toBe("N0");
+  });
+
+  it("matches filed path segments, as breadcrumbs search", () => {
+    expect(searchIndex(ITEMS, "style")[0].note).toBe("N0");
+  });
+
+  it("requires every term to match", () => {
+    expect(searchIndex(ITEMS, "entropy style")).toHaveLength(0);
+    expect(searchIndex(ITEMS, "sparse entropy")[0].note).toBe("N217");
+  });
+
+  it("returns nothing for a blank or unmatched query", () => {
+    expect(searchIndex(ITEMS, "  ")).toHaveLength(0);
+    expect(searchIndex(ITEMS, "zebra")).toHaveLength(0);
+  });
+
+  it("finds docs", () => {
+    expect(searchIndex(ITEMS, "wikilinks")[0].url).toBe("/docs/authoring/wikilink
s/");
+  });
+});

diff --git a/ooknet-design/src/lib/search.te
st.ts b/ooknet-design/src/lib/search.test.ts
new file mode 100644
index 0000000..1275886
--- /dev/null
+++ b/ooknet-design/src/lib/search.test.ts
@@ -0,0 +1,40 @@
+import { describe, expect, it } from "vites
t";
+import { searchIndex, type SearchItem } fro
m "./search";
+
+const ITEMS: SearchItem[] = [
+  { note: "N217", title: "ENTROPY BOUNDS IN
 SPARSE GRAPHS", category: "graph-theory", t
ags: ["entropy", "sparse-graphs"], filed: "/
INFO/GRAPH/ENTROPY/BOUND", url: "/notes/n217
-entropy-bounds/" },
+  { note: "N0", title: "STYLE SHEET", categ
ory: "design-system", tags: ["design-system"
, "reference"], filed: "/META/STYLE/SHEET", 
url: "/notes/n0-style-sheet/" },
+  { note: "DOC", title: "Wikilinks", catego
ry: "docs", tags: [], filed: "/DOCS/AUTHORIN
G/WIKILINKS", url: "/docs/authoring/wikilink
s/" },
+];
+
+describe("searchIndex", () => {
+  it("matches note numbers first", () => {
+    expect(searchIndex(ITEMS, "n217")[0].ur
l).toBe("/notes/n217-entropy-bounds/");
+  });
+
+  it("matches title substrings", () => {
+    expect(searchIndex(ITEMS, "entropy")[0]
.note).toBe("N217");
+  });
+
+  it("matches tags exactly, as tag badges s
earch", () => {
+    expect(searchIndex(ITEMS, "reference")[
0].note).toBe("N0");
+  });
+
+  it("matches filed path segments, as bread
crumbs search", () => {
+    expect(searchIndex(ITEMS, "style")[0].n
ote).toBe("N0");
+  });
+
+  it("requires every term to match", () => 
{
+    expect(searchIndex(ITEMS, "entropy styl
e")).toHaveLength(0);
+    expect(searchIndex(ITEMS, "sparse entro
py")[0].note).toBe("N217");
+  });
+
+  it("returns nothing for a blank or unmatc
hed query", () => {
+    expect(searchIndex(ITEMS, "  ")).toHave
Length(0);
+    expect(searchIndex(ITEMS, "zebra")).toH
aveLength(0);
+  });
+
+  it("finds docs", () => {
+    expect(searchIndex(ITEMS, "wikilinks")[
0].url).toBe("/docs/authoring/wikilinks/");
+  });
+});
 

diff --git a/ooknet-design/src/lib/search.ts b/ooknet-design/src/lib/search.ts
new file mode 100644
index 0000000..a473965
--- /dev/null
+++ b/ooknet-design/src/lib/search.ts
@@ -0,0 +1,47 @@
+// Masthead search scorer. Replaces fuse.js - 9.5k of gzipped fuzzy
+// matching is absurd for an index this size, and a plain scorer stays
+// predictable as it grows. Runs in the browser.
+
+export interface SearchItem {
+  note: string;
+  title: string;
+  category: string;
+  tags: string[];
+  filed: string;
+  url: string;
+}
+
+function fieldScore(value: string, term: string, weight: number): number {
+  const v = value.toLowerCase();
+  if (v === term) return weight * 3;
+  if (v.startsWith(term)) return weight * 2;
+  if (v.includes(term)) return weight;
+  return 0;
+}
+
+function itemScore(item: SearchItem, terms: string[]): number {
+  let total = 0;
+  for (const t of terms) {
+    const s = Math.max(
+      fieldScore(item.note, t, 3),
+      fieldScore(item.title, t, 2),
+      ...item.tags.map((tag) => fieldScore(tag, t, 1.5)),
+      fieldScore(item.category, t, 1),
+      fieldScore(item.filed, t, 1),
+    );
+    if (s === 0) return 0; // every term must land somewhere
+    total += s;
+  }
+  return total;
+}
+
+export function searchIndex(items: SearchItem[], query: string, limit = 8): Searc
hItem[] {
+  const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
+  if (!terms.length) return [];
+  return items
+    .map((item) => ({ item, score: itemScore(item, terms) }))
+    .filter((s) => s.score > 0)
+    .sort((a, b) => b.score - a.score)
+    .slice(0, limit)
+    .map((s) => s.item);
+}

diff --git a/ooknet-design/src/lib/search.ts
 b/ooknet-design/src/lib/search.ts
new file mode 100644
index 0000000..a473965
--- /dev/null
+++ b/ooknet-design/src/lib/search.ts
@@ -0,0 +1,47 @@
+// Masthead search scorer. Replaces fuse.js
 - 9.5k of gzipped fuzzy
+// matching is absurd for an index this siz
e, and a plain scorer stays
+// predictable as it grows. Runs in the bro
wser.
+
+export interface SearchItem {
+  note: string;
+  title: string;
+  category: string;
+  tags: string[];
+  filed: string;
+  url: string;
+}
+
+function fieldScore(value: string, term: st
ring, weight: number): number {
+  const v = value.toLowerCase();
+  if (v === term) return weight * 3;
+  if (v.startsWith(term)) return weight * 2
;
+  if (v.includes(term)) return weight;
+  return 0;
+}
+
+function itemScore(item: SearchItem, terms:
 string[]): number {
+  let total = 0;
+  for (const t of terms) {
+    const s = Math.max(
+      fieldScore(item.note, t, 3),
+      fieldScore(item.title, t, 2),
+      ...item.tags.map((tag) => fieldScore(
tag, t, 1.5)),
+      fieldScore(item.category, t, 1),
+      fieldScore(item.filed, t, 1),
+    );
+    if (s === 0) return 0; // every term mu
st land somewhere
+    total += s;
+  }
+  return total;
+}
+
+export function searchIndex(items: SearchIt
em[], query: string, limit = 8): SearchItem[
] {
+  const terms = query.trim().toLowerCase().
split(/\s+/).filter(Boolean);
+  if (!terms.length) return [];
+  return items
+    .map((item) => ({ item, score: itemScor
e(item, terms) }))
+    .filter((s) => s.score > 0)
+    .sort((a, b) => b.score - a.score)
+    .slice(0, limit)
+    .map((s) => s.item);
+}
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET