about summary refs log tree commit diff
path: root/src/librustdoc/html/static/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/librustdoc/html/static/js')
-rw-r--r--src/librustdoc/html/static/js/main.js433
-rw-r--r--src/librustdoc/html/static/js/rustdoc.d.ts273
-rw-r--r--src/librustdoc/html/static/js/scrape-examples.js45
-rw-r--r--src/librustdoc/html/static/js/search.js5275
-rw-r--r--src/librustdoc/html/static/js/settings.js86
-rw-r--r--src/librustdoc/html/static/js/storage.js69
-rw-r--r--src/librustdoc/html/static/js/stringdex.d.ts165
-rw-r--r--src/librustdoc/html/static/js/stringdex.js3217
-rw-r--r--src/librustdoc/html/static/js/tsconfig.json2
9 files changed, 6236 insertions, 3329 deletions
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index 8e3d07b3a1c..4fcba5f120b 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -54,23 +54,6 @@ function showMain() {
 window.rootPath = getVar("root-path");
 window.currentCrate = getVar("current-crate");
 
-function setMobileTopbar() {
-    // FIXME: It would be nicer to generate this text content directly in HTML,
-    // but with the current code it's hard to get the right information in the right place.
-    const mobileTopbar = document.querySelector(".mobile-topbar");
-    const locationTitle = document.querySelector(".sidebar h2.location");
-    if (mobileTopbar) {
-        const mobileTitle = document.createElement("h2");
-        mobileTitle.className = "location";
-        if (hasClass(document.querySelector(".rustdoc"), "crate")) {
-            mobileTitle.innerHTML = `Crate <a href="#">${window.currentCrate}</a>`;
-        } else if (locationTitle) {
-            mobileTitle.innerHTML = locationTitle.innerHTML;
-        }
-        mobileTopbar.appendChild(mobileTitle);
-    }
-}
-
 /**
  * Gets the human-readable string for the virtual-key code of the
  * given KeyboardEvent, ev.
@@ -84,6 +67,7 @@ function setMobileTopbar() {
  * So I guess you could say things are getting pretty interoperable.
  *
  * @param {KeyboardEvent} ev
+ * @returns {string}
  */
 function getVirtualKey(ev) {
     if ("key" in ev && typeof ev.key !== "undefined") {
@@ -98,18 +82,8 @@ function getVirtualKey(ev) {
 }
 
 const MAIN_ID = "main-content";
-const SETTINGS_BUTTON_ID = "settings-menu";
 const ALTERNATIVE_DISPLAY_ID = "alternative-display";
 const NOT_DISPLAYED_ID = "not-displayed";
-const HELP_BUTTON_ID = "help-button";
-
-function getSettingsButton() {
-    return document.getElementById(SETTINGS_BUTTON_ID);
-}
-
-function getHelpButton() {
-    return document.getElementById(HELP_BUTTON_ID);
-}
 
 // Returns the current URL without any query parameter or hash.
 function getNakedUrl() {
@@ -174,7 +148,7 @@ function getNotDisplayedElem() {
  * contains the displayed element (there can be only one at the same time!). So basically, we switch
  * elements between the two `<section>` elements.
  *
- * @param {HTMLElement|null} elemToDisplay
+ * @param {Element|null} elemToDisplay
  */
 function switchDisplayedElement(elemToDisplay) {
     const el = getAlternativeDisplayElem();
@@ -239,14 +213,14 @@ function preLoadCss(cssUrl) {
         document.head.append(script);
     }
 
-    const settingsButton = getSettingsButton();
-    if (settingsButton) {
-        settingsButton.onclick = event => {
+    onEachLazy(document.querySelectorAll(".settings-menu"), settingsMenu => {
+        /** @param {MouseEvent} event */
+        settingsMenu.querySelector("a").onclick = event => {
             if (event.ctrlKey || event.altKey || event.metaKey) {
                 return;
             }
             window.hideAllModals(false);
-            addClass(getSettingsButton(), "rotate");
+            addClass(settingsMenu, "rotate");
             event.preventDefault();
             // Sending request for the CSS and the JS files at the same time so it will
             // hopefully be loaded when the JS will generate the settings content.
@@ -268,15 +242,42 @@ function preLoadCss(cssUrl) {
                 }
             }, 0);
         };
-    }
+    });
 
     window.searchState = {
         rustdocToolbar: document.querySelector("rustdoc-toolbar"),
         loadingText: "Loading search results...",
-        // This will always be an HTMLInputElement, but tsc can't see that
-        // @ts-expect-error
-        input: document.getElementsByClassName("search-input")[0],
-        outputElement: () => {
+        inputElement: () => {
+            let el = document.getElementsByClassName("search-input")[0];
+            if (!el) {
+                const out = nonnull(nonnull(window.searchState.outputElement()).parentElement);
+                const hdr = document.createElement("div");
+                hdr.className = "main-heading search-results-main-heading";
+                const params = window.searchState.getQueryStringParams();
+                const autofocusParam = params.search === "" ? "autofocus" : "";
+                hdr.innerHTML = `<nav class="sub">
+                    <form class="search-form loading">
+                        <span></span> <!-- This empty span is a hacky fix for Safari: see #93184 -->
+                        <input
+                            ${autofocusParam}
+                            class="search-input"
+                            name="search"
+                            aria-label="Run search in the documentation"
+                            autocomplete="off"
+                            spellcheck="false"
+                            placeholder="Type ‘S’ or ‘/’ to search, ‘?’ for more options…"
+                            type="search">
+                    </form>
+                </nav><div class="search-switcher"></div>`;
+                out.insertBefore(hdr, window.searchState.outputElement());
+                el = document.getElementsByClassName("search-input")[0];
+            }
+            if (el instanceof HTMLInputElement) {
+                return el;
+            }
+            return null;
+        },
+        containerElement: () => {
             let el = document.getElementById("search");
             if (!el) {
                 el = document.createElement("section");
@@ -285,6 +286,19 @@ function preLoadCss(cssUrl) {
             }
             return el;
         },
+        outputElement: () => {
+            const container = window.searchState.containerElement();
+            if (!container) {
+                return null;
+            }
+            let el = container.querySelector(".search-out");
+            if (!el) {
+                el = document.createElement("div");
+                el.className = "search-out";
+                container.appendChild(el);
+            }
+            return el;
+        },
         title: document.title,
         titleBeforeSearch: document.title,
         timeout: null,
@@ -303,25 +317,52 @@ function preLoadCss(cssUrl) {
             }
         },
         isDisplayed: () => {
-            const outputElement = window.searchState.outputElement();
-            return !!outputElement &&
-                !!outputElement.parentElement &&
-                outputElement.parentElement.id === ALTERNATIVE_DISPLAY_ID;
+            const container = window.searchState.containerElement();
+            if (!container) {
+                return false;
+            }
+            return !!container.parentElement && container.parentElement.id ===
+                ALTERNATIVE_DISPLAY_ID;
         },
         // Sets the focus on the search bar at the top of the page
         focus: () => {
-            window.searchState.input && window.searchState.input.focus();
+            const inputElement = window.searchState.inputElement();
+            window.searchState.showResults();
+            if (inputElement) {
+                inputElement.focus();
+                // Avoid glitch if something focuses the search button after clicking.
+                requestAnimationFrame(() => inputElement.focus());
+            }
         },
         // Removes the focus from the search bar.
         defocus: () => {
-            window.searchState.input && window.searchState.input.blur();
+            nonnull(window.searchState.inputElement()).blur();
         },
-        showResults: search => {
-            if (search === null || typeof search === "undefined") {
-                search = window.searchState.outputElement();
+        toggle: () => {
+            if (window.searchState.isDisplayed()) {
+                window.searchState.defocus();
+                window.searchState.hideResults();
+            } else {
+                window.searchState.focus();
             }
-            switchDisplayedElement(search);
+        },
+        showResults: () => {
             document.title = window.searchState.title;
+            if (window.searchState.isDisplayed()) {
+                return;
+            }
+            const search = window.searchState.containerElement();
+            switchDisplayedElement(search);
+            const btn = document.querySelector("#search-button a");
+            if (browserSupportsHistoryApi() && btn instanceof HTMLAnchorElement &&
+                window.searchState.getQueryStringParams().search === undefined
+            ) {
+                history.pushState(null, "", btn.href);
+            }
+            const btnLabel = document.querySelector("#search-button a span.label");
+            if (btnLabel) {
+                btnLabel.innerHTML = "Exit";
+            }
         },
         removeQueryParameters: () => {
             // We change the document title.
@@ -334,6 +375,10 @@ function preLoadCss(cssUrl) {
             switchDisplayedElement(null);
             // We also remove the query parameter from the URL.
             window.searchState.removeQueryParameters();
+            const btnLabel = document.querySelector("#search-button a span.label");
+            if (btnLabel) {
+                btnLabel.innerHTML = "Search";
+            }
         },
         getQueryStringParams: () => {
             /** @type {Object.<any, string>} */
@@ -348,11 +393,11 @@ function preLoadCss(cssUrl) {
             return params;
         },
         setup: () => {
-            const search_input = window.searchState.input;
+            let searchLoaded = false;
+            const search_input = window.searchState.inputElement();
             if (!search_input) {
                 return;
             }
-            let searchLoaded = false;
             // If you're browsing the nightly docs, the page might need to be refreshed for the
             // search to work because the hash of the JS scripts might have changed.
             function sendSearchForm() {
@@ -362,22 +407,101 @@ function preLoadCss(cssUrl) {
             function loadSearch() {
                 if (!searchLoaded) {
                     searchLoaded = true;
+                    window.rr_ = data => {
+                        window.searchIndex = data;
+                    };
+                    if (!window.StringdexOnload) {
+                        window.StringdexOnload = [];
+                    }
+                    window.StringdexOnload.push(() => {
+                        loadScript(
+                            // @ts-expect-error
+                            getVar("static-root-path") + getVar("search-js"),
+                            sendSearchForm,
+                        );
+                    });
                     // @ts-expect-error
-                    loadScript(getVar("static-root-path") + getVar("search-js"), sendSearchForm);
-                    loadScript(resourcePath("search-index", ".js"), sendSearchForm);
+                    loadScript(getVar("static-root-path") + getVar("stringdex-js"), sendSearchForm);
+                    loadScript(resourcePath("search.index/root", ".js"), sendSearchForm);
                 }
             }
 
             search_input.addEventListener("focus", () => {
-                window.searchState.origPlaceholder = search_input.placeholder;
-                search_input.placeholder = "Type your search here.";
                 loadSearch();
             });
 
-            if (search_input.value !== "") {
-                loadSearch();
+            const btn = document.getElementById("search-button");
+            if (btn) {
+                btn.onclick = event => {
+                    if (event.ctrlKey || event.altKey || event.metaKey) {
+                        return;
+                    }
+                    event.preventDefault();
+                    window.searchState.toggle();
+                    loadSearch();
+                };
             }
 
+            // Push and pop states are used to add search results to the browser
+            // history.
+            if (browserSupportsHistoryApi()) {
+                // Store the previous <title> so we can revert back to it later.
+                const previousTitle = document.title;
+
+                window.addEventListener("popstate", e => {
+                    const params = window.searchState.getQueryStringParams();
+                    // Revert to the previous title manually since the History
+                    // API ignores the title parameter.
+                    document.title = previousTitle;
+                    // Synchronize search bar with query string state and
+                    // perform the search. This will empty the bar if there's
+                    // nothing there, which lets you really go back to a
+                    // previous state with nothing in the bar.
+                    const inputElement = window.searchState.inputElement();
+                    if (params.search !== undefined && inputElement !== null) {
+                        loadSearch();
+                        inputElement.value = params.search;
+                        // Some browsers fire "onpopstate" for every page load
+                        // (Chrome), while others fire the event only when actually
+                        // popping a state (Firefox), which is why search() is
+                        // called both here and at the end of the startSearch()
+                        // function.
+                        e.preventDefault();
+                        window.searchState.showResults();
+                        if (params.search === "") {
+                            window.searchState.focus();
+                        }
+                    } else {
+                        // When browsing back from search results the main page
+                        // visibility must be reset.
+                        window.searchState.hideResults();
+                    }
+                });
+            }
+
+            // This is required in firefox to avoid this problem: Navigating to a search result
+            // with the keyboard, hitting enter, and then hitting back would take you back to
+            // the doc page, rather than the search that should overlay it.
+            // This was an interaction between the back-forward cache and our handlers
+            // that try to sync state between the URL and the search input. To work around it,
+            // do a small amount of re-init on page show.
+            window.onpageshow = () => {
+                const inputElement = window.searchState.inputElement();
+                const qSearch = window.searchState.getQueryStringParams().search;
+                if (qSearch !== undefined && inputElement !== null) {
+                    if (inputElement.value === "") {
+                        inputElement.value = qSearch;
+                    }
+                    window.searchState.showResults();
+                    if (qSearch === "") {
+                        loadSearch();
+                        window.searchState.focus();
+                    }
+                } else {
+                    window.searchState.hideResults();
+                }
+            };
+
             const params = window.searchState.getQueryStringParams();
             if (params.search !== undefined) {
                 window.searchState.setLoadingSearch();
@@ -386,13 +510,9 @@ function preLoadCss(cssUrl) {
         },
         setLoadingSearch: () => {
             const search = window.searchState.outputElement();
-            if (!search) {
-                return;
-            }
-            search.innerHTML = "<h3 class=\"search-loading\">" +
-                window.searchState.loadingText +
-                "</h3>";
-            window.searchState.showResults(search);
+            nonnull(search).innerHTML = "<h3 class=\"search-loading\">" +
+                window.searchState.loadingText + "</h3>";
+            window.searchState.showResults();
         },
         descShards: new Map(),
         loadDesc: async function({descShard, descIndex}) {
@@ -1155,13 +1275,11 @@ function preLoadCss(cssUrl) {
     }
 
     window.addEventListener("resize", () => {
-        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT) {
             // As a workaround to the behavior of `contains: layout` used in doc togglers,
             // tooltip popovers are positioned using javascript.
             //
             // This means when the window is resized, we need to redo the layout.
-            // @ts-expect-error
             const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE;
             const force_visible = base.TOOLTIP_FORCE_VISIBLE;
             hideTooltip(false);
@@ -1207,11 +1325,9 @@ function preLoadCss(cssUrl) {
      */
     function showTooltip(e) {
         const notable_ty = e.getAttribute("data-notable-ty");
-        // @ts-expect-error
         if (!window.NOTABLE_TRAITS && notable_ty) {
             const data = document.getElementById("notable-traits-data");
             if (data) {
-                // @ts-expect-error
                 window.NOTABLE_TRAITS = JSON.parse(data.innerText);
             } else {
                 throw new Error("showTooltip() called with notable without any notable traits!");
@@ -1219,14 +1335,15 @@ function preLoadCss(cssUrl) {
         }
         // Make this function idempotent. If the tooltip is already shown, avoid doing extra work
         // and leave it alone.
-        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
-            // @ts-expect-error
             clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
             return;
         }
         window.hideAllModals(false);
-        const wrapper = document.createElement("div");
+        // use Object.assign to make sure the object has the correct type
+        // with all of the correct fields before it is assigned to a variable,
+        // as typescript has no way to change the type of a variable once it is initialized.
+        const wrapper = Object.assign(document.createElement("div"), {TOOLTIP_BASE: e});
         if (notable_ty) {
             wrapper.innerHTML = "<div class=\"content\">" +
                 // @ts-expect-error
@@ -1272,11 +1389,7 @@ function preLoadCss(cssUrl) {
             );
         }
         wrapper.style.visibility = "";
-        // @ts-expect-error
         window.CURRENT_TOOLTIP_ELEMENT = wrapper;
-        // @ts-expect-error
-        window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
-        // @ts-expect-error
         clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
         wrapper.onpointerenter = ev => {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
@@ -1311,19 +1424,15 @@ function preLoadCss(cssUrl) {
      */
     function setTooltipHoverTimeout(element, show) {
         clearTooltipHoverTimeout(element);
-        // @ts-expect-error
         if (!show && !window.CURRENT_TOOLTIP_ELEMENT) {
             // To "hide" an already hidden element, just cancel its timeout.
             return;
         }
-        // @ts-expect-error
         if (show && window.CURRENT_TOOLTIP_ELEMENT) {
             // To "show" an already visible element, just cancel its timeout.
             return;
         }
-        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT &&
-            // @ts-expect-error
             window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) {
             // Don't do anything if another tooltip is already visible.
             return;
@@ -1346,24 +1455,20 @@ function preLoadCss(cssUrl) {
      */
     function clearTooltipHoverTimeout(element) {
         if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) {
-            // @ts-expect-error
             removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
             clearTimeout(element.TOOLTIP_HOVER_TIMEOUT);
             delete element.TOOLTIP_HOVER_TIMEOUT;
         }
     }
 
-    // @ts-expect-error
+    /**
+     * @param {Event & { relatedTarget: Node }} event
+     */
     function tooltipBlurHandler(event) {
-        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT &&
-            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.contains(document.activeElement) &&
-            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.contains(event.relatedTarget) &&
-            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(document.activeElement) &&
-            // @ts-expect-error
             !window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.contains(event.relatedTarget)
         ) {
             // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari.
@@ -1385,30 +1490,22 @@ function preLoadCss(cssUrl) {
      *                          If set to `false`, leave keyboard focus alone.
      */
     function hideTooltip(focus) {
-        // @ts-expect-error
         if (window.CURRENT_TOOLTIP_ELEMENT) {
-            // @ts-expect-error
             if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
                 if (focus) {
-                    // @ts-expect-error
                     window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
                 }
-                // @ts-expect-error
                 window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
             }
-            // @ts-expect-error
             document.body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
-            // @ts-expect-error
             clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT);
-            // @ts-expect-error
-            window.CURRENT_TOOLTIP_ELEMENT = null;
+            window.CURRENT_TOOLTIP_ELEMENT = undefined;
         }
     }
 
     onEachLazy(document.getElementsByClassName("tooltip"), e => {
         e.onclick = () => {
             e.TOOLTIP_FORCE_VISIBLE = e.TOOLTIP_FORCE_VISIBLE ? false : true;
-            // @ts-expect-error
             if (window.CURRENT_TOOLTIP_ELEMENT && !e.TOOLTIP_FORCE_VISIBLE) {
                 hideTooltip(true);
             } else {
@@ -1444,9 +1541,7 @@ function preLoadCss(cssUrl) {
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            // @ts-expect-error
             if (!e.TOOLTIP_FORCE_VISIBLE && window.CURRENT_TOOLTIP_ELEMENT &&
-                // @ts-expect-error
                 !window.CURRENT_TOOLTIP_ELEMENT.contains(ev.relatedTarget)) {
                 // Tooltip pointer leave gesture:
                 //
@@ -1479,7 +1574,6 @@ function preLoadCss(cssUrl) {
                 // * https://www.nngroup.com/articles/tooltip-guidelines/
                 // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
                 setTooltipHoverTimeout(e, false);
-                // @ts-expect-error
                 addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out");
             }
         };
@@ -1500,15 +1594,13 @@ function preLoadCss(cssUrl) {
 
     // @ts-expect-error
     function helpBlurHandler(event) {
-        // @ts-expect-error
-        if (!getHelpButton().contains(document.activeElement) &&
-            // @ts-expect-error
-            !getHelpButton().contains(event.relatedTarget) &&
-            // @ts-expect-error
-            !getSettingsButton().contains(document.activeElement) &&
-            // @ts-expect-error
-            !getSettingsButton().contains(event.relatedTarget)
-        ) {
+        const isInPopover = onEachLazy(
+            document.querySelectorAll(".settings-menu, .help-menu"),
+            menu => {
+                return menu.contains(document.activeElement) || menu.contains(event.relatedTarget);
+            },
+        );
+        if (!isInPopover) {
             window.hidePopoverMenus();
         }
     }
@@ -1529,7 +1621,7 @@ function preLoadCss(cssUrl) {
             ["&#9166;", "Go to active search result"],
             ["+", "Expand all sections"],
             ["-", "Collapse all sections"],
-            // for the sake of brevity, we don't say "inherint impl blocks",
+            // for the sake of brevity, we don't say "inherit impl blocks",
             // although that would be more correct,
             // since trait impl blocks are collapsed by -
             ["_", "Collapse all sections, including impl blocks"],
@@ -1571,10 +1663,9 @@ function preLoadCss(cssUrl) {
 
         const container = document.createElement("div");
         if (!isHelpPage) {
-            container.className = "popover";
+            container.className = "popover content";
         }
         container.id = "help";
-        container.style.display = "none";
 
         const side_by_side = document.createElement("div");
         side_by_side.className = "side-by-side";
@@ -1588,19 +1679,17 @@ function preLoadCss(cssUrl) {
         if (isHelpPage) {
             const help_section = document.createElement("section");
             help_section.appendChild(container);
-            // @ts-expect-error
-            document.getElementById("main-content").appendChild(help_section);
-            container.style.display = "block";
+            nonnull(document.getElementById("main-content")).appendChild(help_section);
         } else {
-            const help_button = getHelpButton();
-            // @ts-expect-error
-            help_button.appendChild(container);
-
-            container.onblur = helpBlurHandler;
-            // @ts-expect-error
-            help_button.onblur = helpBlurHandler;
-            // @ts-expect-error
-            help_button.children[0].onblur = helpBlurHandler;
+            onEachLazy(document.getElementsByClassName("help-menu"), menu => {
+                if (menu.offsetWidth !== 0) {
+                    menu.appendChild(container);
+                    container.onblur = helpBlurHandler;
+                    menu.onblur = helpBlurHandler;
+                    menu.children[0].onblur = helpBlurHandler;
+                    return true;
+                }
+            });
         }
 
         return container;
@@ -1621,80 +1710,57 @@ function preLoadCss(cssUrl) {
      * Hide all the popover menus.
      */
     window.hidePopoverMenus = () => {
-        onEachLazy(document.querySelectorAll("rustdoc-toolbar .popover"), elem => {
+        onEachLazy(document.querySelectorAll(".settings-menu .popover"), elem => {
             elem.style.display = "none";
         });
-        const button = getHelpButton();
-        if (button) {
-            removeClass(button, "help-open");
-        }
+        onEachLazy(document.querySelectorAll(".help-menu .popover"), elem => {
+            elem.parentElement.removeChild(elem);
+        });
     };
 
     /**
-     * Returns the help menu element (not the button).
-     *
-     * @param {boolean} buildNeeded - If this argument is `false`, the help menu element won't be
-     *                                built if it doesn't exist.
-     *
-     * @return {HTMLElement}
-     */
-    function getHelpMenu(buildNeeded) {
-        // @ts-expect-error
-        let menu = getHelpButton().querySelector(".popover");
-        if (!menu && buildNeeded) {
-            menu = buildHelpMenu();
-        }
-        // @ts-expect-error
-        return menu;
-    }
-
-    /**
      * Show the help popup menu.
      */
     function showHelp() {
+        window.hideAllModals(false);
         // Prevent `blur` events from being dispatched as a result of closing
         // other modals.
-        const button = getHelpButton();
-        addClass(button, "help-open");
-        // @ts-expect-error
-        button.querySelector("a").focus();
-        const menu = getHelpMenu(true);
-        if (menu.style.display === "none") {
-            // @ts-expect-error
-            window.hideAllModals();
-            menu.style.display = "";
-        }
+        onEachLazy(document.querySelectorAll(".help-menu a"), menu => {
+            if (menu.offsetWidth !== 0) {
+                menu.focus();
+                return true;
+            }
+        });
+        buildHelpMenu();
     }
 
-    const helpLink = document.querySelector(`#${HELP_BUTTON_ID} > a`);
     if (isHelpPage) {
         buildHelpMenu();
-    } else if (helpLink) {
-        helpLink.addEventListener("click", event => {
-            // By default, have help button open docs in a popover.
-            // If user clicks with a moderator, though, use default browser behavior,
-            // probably opening in a new window or tab.
-            if (!helpLink.contains(helpLink) ||
-                // @ts-expect-error
-                event.ctrlKey ||
-                // @ts-expect-error
-                event.altKey ||
-                // @ts-expect-error
-                event.metaKey) {
-                return;
-            }
-            event.preventDefault();
-            const menu = getHelpMenu(true);
-            const shouldShowHelp = menu.style.display === "none";
-            if (shouldShowHelp) {
-                showHelp();
-            } else {
-                window.hidePopoverMenus();
-            }
+    } else {
+        onEachLazy(document.querySelectorAll(".help-menu > a"), helpLink => {
+            helpLink.addEventListener(
+                "click",
+                /** @param {MouseEvent} event */
+                event => {
+                    // By default, have help button open docs in a popover.
+                    // If user clicks with a moderator, though, use default browser behavior,
+                    // probably opening in a new window or tab.
+                    if (event.ctrlKey ||
+                        event.altKey ||
+                        event.metaKey) {
+                        return;
+                    }
+                    event.preventDefault();
+                    if (document.getElementById("help")) {
+                        window.hidePopoverMenus();
+                    } else {
+                        showHelp();
+                    }
+                },
+            );
         });
     }
 
-    setMobileTopbar();
     addSidebarItems();
     addSidebarCrates();
     onHashChange(null);
@@ -1746,13 +1812,20 @@ function preLoadCss(cssUrl) {
     // On larger, "desktop-sized" viewports (though that includes many
     // tablets), it's fixed-position, appears in the left side margin,
     // and it can be activated by resizing the sidebar into nothing.
-    const sidebarButton = document.getElementById("sidebar-button");
+    let sidebarButton = document.getElementById("sidebar-button");
+    const body = document.querySelector(".main-heading");
+    if (!sidebarButton && body) {
+        sidebarButton = document.createElement("div");
+        sidebarButton.id = "sidebar-button";
+        const path = `${window.rootPath}${window.currentCrate}/all.html`;
+        sidebarButton.innerHTML = `<a href="${path}" title="show sidebar"></a>`;
+        body.insertBefore(sidebarButton, body.firstChild);
+    }
     if (sidebarButton) {
         sidebarButton.addEventListener("click", e => {
             removeClass(document.documentElement, "hide-sidebar");
             updateLocalStorage("hide-sidebar", "false");
-            if (document.querySelector(".rustdoc.src")) {
-                // @ts-expect-error
+            if (window.rustdocToggleSrcSidebar) {
                 window.rustdocToggleSrcSidebar();
             }
             e.preventDefault();
diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts
index b082b65ab57..3ac10742e41 100644
--- a/src/librustdoc/html/static/js/rustdoc.d.ts
+++ b/src/librustdoc/html/static/js/rustdoc.d.ts
@@ -2,6 +2,8 @@
 // not put into the JavaScript we include as part of the documentation. It is used for
 // type checking. See README.md in this directory for more info.
 
+import { RoaringBitmap } from "./stringdex";
+
 /* eslint-disable */
 declare global {
     /** Search engine data used by main.js and search.js */
@@ -10,11 +12,25 @@ declare global {
     declare function nonnull(x: T|null, msg: string|undefined);
     /** Defined and documented in `storage.js` */
     declare function nonundef(x: T|undefined, msg: string|undefined);
+    interface PromiseConstructor {
+        /**
+         * Polyfill
+         * @template T
+         */
+        withResolvers: function(): {
+            "promise": Promise<T>,
+            "resolve": (function(T): void),
+            "reject": (function(any): void)
+        };
+    }
     interface Window {
         /** Make the current theme easy to find */
         currentTheme: HTMLLinkElement|null;
         /** Generated in `render/context.rs` */
         SIDEBAR_ITEMS?: { [key: string]: string[] };
+        /** Notable trait data */
+        NOTABLE_TRAITS?: { [key: string]: string };
+        CURRENT_TOOLTIP_ELEMENT?: HTMLElement & { TOOLTIP_BASE: HTMLElement };
         /** Used by the popover tooltip code. */
         RUSTDOC_TOOLTIP_HOVER_MS: number;
         /** Used by the popover tooltip code. */
@@ -80,6 +96,10 @@ declare global {
         pending_type_impls?: rustdoc.TypeImpls,
         rustdoc_add_line_numbers_to_examples?: function(),
         rustdoc_remove_line_numbers_from_examples?: function(),
+        /** JSON-encoded raw search index */
+        searchIndex: string,
+        /** Used in search index shards in order to load data into the in-memory database */
+        rr_: function(string),
     }
     interface HTMLElement {
         /** Used by the popover tooltip code. */
@@ -95,29 +115,28 @@ declare namespace rustdoc {
     interface SearchState {
         rustdocToolbar: HTMLElement|null;
         loadingText: string;
-        input: HTMLInputElement|null;
+        inputElement: function(): HTMLInputElement|null;
+        containerElement: function(): Element|null;
         title: string;
         titleBeforeSearch: string;
-        timeout: number|null;
+        timeout: ReturnType<typeof setTimeout>|null;
         currentTab: number;
-        focusedByTab: [number|null, number|null, number|null];
+        focusedByTab: [Element|null, Element|null, Element|null];
         clearInputTimeout: function;
-        outputElement(): HTMLElement|null;
-        focus();
-        defocus();
-        // note: an optional param is not the same as
-        // a nullable/undef-able param.
-        showResults(elem?: HTMLElement|null);
-        removeQueryParameters();
-        hideResults();
-        getQueryStringParams(): Object.<any, string>;
-        origPlaceholder: string;
+        outputElement: function(): Element|null;
+        focus: function();
+        defocus: function();
+        toggle: function();
+        showResults: function();
+        removeQueryParameters: function();
+        hideResults: function();
+        getQueryStringParams: function(): Object.<any, string>;
         setup: function();
         setLoadingSearch();
         descShards: Map<string, SearchDescShard[]>;
         loadDesc: function({descShard: SearchDescShard, descIndex: number}): Promise<string|null>;
-        loadedDescShard(string, number, string);
-        isDisplayed(): boolean,
+        loadedDescShard: function(string, number, string);
+        isDisplayed: function(): boolean;
     }
 
     interface SearchDescShard {
@@ -129,14 +148,15 @@ declare namespace rustdoc {
 
     /**
      * A single parsed "atom" in a search query. For example,
-     *
+     * 
      *     std::fmt::Formatter, Write -> Result<()>
-     *     ┏━━━━━━━━━━━━━━━━━━  ┌────    ┏━━━━━┅┅┅┅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
-     *     ┃                    │        ┗ QueryElement {        ┊
-     *     ┃                    │              name: Result      ┊
-     *     ┃                    │              generics: [       ┊
-     *     ┃                    │                   QueryElement ┘
-     *     ┃                    │                   name: ()
+     *     ┏━━━━━━━━━━━━━━━━━━  ┌────    ┏━━━━━┅┅┅┅┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐
+     *     ┃                    │        ┗ QueryElement {          ┊
+     *     ┃                    │              name: Result        ┊
+     *     ┃                    │              generics: [         ┊
+     *     ┃                    │                   QueryElement { ┘
+     *     ┃                    │                       name: ()
+     *     ┃                    │                   }
      *     ┃                    │              ]
      *     ┃                    │          }
      *     ┃                    └ QueryElement {
@@ -156,14 +176,14 @@ declare namespace rustdoc {
         normalizedPathLast: string,
         generics: Array<QueryElement>,
         bindings: Map<number, Array<QueryElement>>,
-        typeFilter: number|null,
+        typeFilter: number,
     }
 
     /**
      * Same as QueryElement, but bindings and typeFilter support strings
      */
     interface ParserQueryElement {
-        name: string|null,
+        name: string,
         id: number|null,
         fullPath: Array<string>,
         pathWithoutLast: Array<string>,
@@ -172,7 +192,7 @@ declare namespace rustdoc {
         generics: Array<ParserQueryElement>,
         bindings: Map<string, Array<ParserQueryElement>>,
         bindingName: {name: string|null, generics: ParserQueryElement[]}|null,
-        typeFilter: number|string|null,
+        typeFilter: string|null,
     }
 
     /**
@@ -215,35 +235,74 @@ declare namespace rustdoc {
     /**
      * An entry in the search index database.
      */
+    interface EntryData {
+        krate: number,
+        ty: ItemType,
+        modulePath: number?,
+        exactModulePath: number?,
+        parent: number?,
+        deprecated: boolean,
+        associatedItemDisambiguator: string?,
+    }
+
+    /**
+     * A path in the search index database
+     */
+    interface PathData {
+        ty: ItemType,
+        modulePath: string,
+        exactModulePath: string?,
+    }
+
+    /**
+     * A function signature in the search index database
+     *
+     * Note that some non-function items (eg. constants, struct fields) have a function signature so they can appear in type-based search.
+     */
+    interface FunctionData {
+        functionSignature: FunctionSearchType|null,
+        paramNames: string[],
+        elemCount: number,
+    }
+
+    /**
+     * A function signature in the search index database
+     */
+    interface TypeData {
+        searchUnbox: boolean,
+        invertedFunctionSignatureIndex: RoaringBitmap[],
+    }
+
+    /**
+     * A search entry of some sort.
+     */
     interface Row {
-        crate: string,
-        descShard: SearchDescShard,
         id: number,
-        // This is the name of the item. For doc aliases, if you want the name of the aliased
-        // item, take a look at `Row.original.name`.
+        crate: string,
+        ty: ItemType,
         name: string,
         normalizedName: string,
-        word: string,
-        paramNames: string[],
-        parent: ({ty: number, name: string, path: string, exactPath: string}|null|undefined),
-        path: string,
-        ty: number,
-        type: FunctionSearchType | null,
-        descIndex: number,
-        bitIndex: number,
-        implDisambiguator: String | null,
-        is_alias?: boolean,
-        original?: Row,
+        modulePath: string,
+        exactModulePath: string,
+        entry: EntryData?,
+        path: PathData?,
+        type: FunctionData?,
+        deprecated: boolean,
+        parent: { path: PathData, name: string}?,
     }
 
+    type ItemType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
+        11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
+        21 | 22 | 23 | 24 | 25 | 26;
+
     /**
      * The viewmodel for the search engine results page.
      */
     interface ResultsTable {
-        in_args: Array<ResultObject>,
-        returned: Array<ResultObject>,
-        others: Array<ResultObject>,
-        query: ParsedQuery,
+        in_args: AsyncGenerator<ResultObject>,
+        returned: AsyncGenerator<ResultObject>,
+        others: AsyncGenerator<ResultObject>,
+        query: ParsedQuery<rustdoc.ParserQueryElement>,
     }
 
     type Results = { max_dist?: number } & Map<number, ResultObject>
@@ -252,25 +311,41 @@ declare namespace rustdoc {
      * An annotated `Row`, used in the viewmodel.
      */
     interface ResultObject {
-        desc: string,
+        desc: Promise<string|null>,
         displayPath: string,
         fullPath: string,
         href: string,
         id: number,
         dist: number,
         path_dist: number,
-        name: string,
-        normalizedName: string,
-        word: string,
         index: number,
-        parent: (Object|undefined),
-        path: string,
-        ty: number,
+        parent: ({
+            path: string,
+            exactPath: string,
+            name: string,
+            ty: number,
+        }|undefined),
         type?: FunctionSearchType,
         paramNames?: string[],
         displayTypeSignature: Promise<rustdoc.DisplayTypeSignature> | null,
         item: Row,
-        dontValidate?: boolean,
+        is_alias: boolean,
+        alias?: string,
+    }
+
+    /**
+     * An annotated `Row`, used in the viewmodel.
+     */
+    interface PlainResultObject {
+        id: number,
+        dist: number,
+        path_dist: number,
+        index: number,
+        elems: rustdoc.QueryElement[],
+        returned: rustdoc.QueryElement[],
+        is_alias: boolean,
+        alias?: string,
+        original?: rustdoc.Rlow,
     }
 
     /**
@@ -364,7 +439,19 @@ declare namespace rustdoc {
      * Numeric IDs are *ONE-indexed* into the paths array (`p`). Zero is used as a sentinel for `null`
      * because `null` is four bytes while `0` is one byte.
      */
-    type RawFunctionType = number | [number, Array<RawFunctionType>];
+    type RawFunctionType = number | [number, Array<RawFunctionType>] | [number, Array<RawFunctionType>, Array<[RawFunctionType, RawFunctionType[]]>];
+
+    /**
+     * Utility typedef for deserializing compact JSON.
+     *
+     * R is the required part, O is the optional part, which goes afterward.
+     * For example, `ArrayWithOptionals<[A, B], [C, D]>` matches
+     * `[A, B] | [A, B, C] | [A, B, C, D]`.
+     */
+    type ArrayWithOptionals<R extends any[], O extends any[]> =
+        O extends [infer First, ...infer Rest] ?
+            R | ArrayWithOptionals<[...R, First], Rest> :
+            R;
 
     /**
      * The type signature entry in the decoded search index.
@@ -382,8 +469,8 @@ declare namespace rustdoc {
      */
     interface FunctionType {
         id: null|number,
-        ty: number|null,
-        name?: string,
+        ty: ItemType,
+        name: string|null,
         path: string|null,
         exactPath: string|null,
         unboxFlag: boolean,
@@ -403,73 +490,6 @@ declare namespace rustdoc {
         bindings: Map<number, FingerprintableType[]>;
     };
 
-    /**
-     * The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f`
-     * are arrays with the same length. `q`, `a`, and `c` use a sparse
-     * representation for compactness.
-     *
-     * `n[i]` contains the name of an item.
-     *
-     * `t[i]` contains the type of that item
-     * (as a string of characters that represent an offset in `itemTypes`).
-     *
-     * `d[i]` contains the description of that item.
-     *
-     * `q` contains the full paths of the items. For compactness, it is a set of
-     * (index, path) pairs used to create a map. If a given index `i` is
-     * not present, this indicates "same as the last index present".
-     *
-     * `i[i]` contains an item's parent, usually a module. For compactness,
-     * it is a set of indexes into the `p` array.
-     *
-     * `f` contains function signatures, or `0` if the item isn't a function.
-     * More information on how they're encoded can be found in rustc-dev-guide
-     *
-     * Functions are themselves encoded as arrays. The first item is a list of
-     * types representing the function's inputs, and the second list item is a list
-     * of types representing the function's output. Tuples are flattened.
-     * Types are also represented as arrays; the first item is an index into the `p`
-     * array, while the second is a list of types representing any generic parameters.
-     *
-     * b[i] contains an item's impl disambiguator. This is only present if an item
-     * is defined in an impl block and, the impl block's type has more than one associated
-     * item with the same name.
-     *
-     * `a` defines aliases with an Array of pairs: [name, offset], where `offset`
-     * points into the n/t/d/q/i/f arrays.
-     *
-     * `doc` contains the description of the crate.
-     *
-     * `p` is a list of path/type pairs. It is used for parents and function parameters.
-     * The first item is the type, the second is the name, the third is the visible path (if any) and
-     * the fourth is the canonical path used for deduplication (if any).
-     *
-     * `r` is the canonical path used for deduplication of re-exported items.
-     * It is not used for associated items like methods (that's the fourth element
-     * of `p`) but is used for modules items like free functions.
-     *
-     * `c` is an array of item indices that are deprecated.
-     *
-     * `u` is an array of item indices that are unstable.
-     */
-    type RawSearchIndexCrate = {
-    doc: string,
-    a: { [key: string]: number[] },
-    n: Array<string>,
-    t: string,
-    D: string,
-    e: string,
-    q: Array<[number, string]>,
-    i: string,
-    f: string,
-    p: Array<[number, string] | [number, string, number] | [number, string, number, number] | [number, string, number, number, string]>,
-    b: Array<[number, String]>,
-    c: string,
-    u: string,
-    r: Array<[number, number]>,
-    P: Array<[number, string]>,
-    };
-
     type VlqData = VlqData[] | number;
 
     /**
@@ -501,4 +521,13 @@ declare namespace rustdoc {
         options?: string[],
         default: string | boolean,
     }
+
+    /**
+     * Single element in the data-locs field of a scraped example.
+     * First field is the start and end char index,
+     * other fields seem to be unused.
+     *
+     * Generated by `render_call_locations` in `render/mod.rs`.
+     */
+    type ScrapedLoc = [[number, number], string, string]
 }
diff --git a/src/librustdoc/html/static/js/scrape-examples.js b/src/librustdoc/html/static/js/scrape-examples.js
index d641405c875..eeab591bcd8 100644
--- a/src/librustdoc/html/static/js/scrape-examples.js
+++ b/src/librustdoc/html/static/js/scrape-examples.js
@@ -1,7 +1,4 @@
-/* global addClass, hasClass, removeClass, onEachLazy */
-
-// Eventually fix this.
-// @ts-nocheck
+ /* global addClass, hasClass, removeClass, onEachLazy, nonnull */
 
 "use strict";
 
@@ -14,8 +11,16 @@
     const DEFAULT_MAX_LINES = 5;
     const HIDDEN_MAX_LINES = 10;
 
-    // Scroll code block to the given code location
+    /**
+     * Scroll code block to the given code location
+     * @param {HTMLElement} elt
+     * @param {[number, number]} loc
+     * @param {boolean} isHidden
+     */
     function scrollToLoc(elt, loc, isHidden) {
+        /** @type {HTMLElement[]} */
+        // blocked on https://github.com/microsoft/TypeScript/issues/29037
+        // @ts-expect-error
         const lines = elt.querySelectorAll("[data-nosnippet]");
         let scrollOffset;
 
@@ -35,10 +40,15 @@
             scrollOffset = offsetMid - halfHeight;
         }
 
-        lines[0].parentElement.scrollTo(0, scrollOffset);
-        elt.querySelector(".rust").scrollTo(0, scrollOffset);
+        nonnull(lines[0].parentElement).scrollTo(0, scrollOffset);
+        nonnull(elt.querySelector(".rust")).scrollTo(0, scrollOffset);
     }
 
+    /**
+     * @param {HTMLElement} parent
+     * @param {string} className
+     * @param {string} content
+     */
     function createScrapeButton(parent, className, content) {
         const button = document.createElement("button");
         button.className = className;
@@ -50,20 +60,24 @@
     window.updateScrapedExample = (example, buttonHolder) => {
         let locIndex = 0;
         const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight"));
-        const link = example.querySelector(".scraped-example-title a");
+
+        /** @type {HTMLAnchorElement} */
+        const link = nonnull(example.querySelector(".scraped-example-title a"));
         let expandButton = null;
 
         if (!example.classList.contains("expanded")) {
             expandButton = createScrapeButton(buttonHolder, "expand", "Show all");
         }
-        const isHidden = example.parentElement.classList.contains("more-scraped-examples");
+        const isHidden = nonnull(example.parentElement).classList.contains("more-scraped-examples");
 
+        // @ts-expect-error
         const locs = example.locs;
         if (locs.length > 1) {
             const next = createScrapeButton(buttonHolder, "next", "Next usage");
             const prev = createScrapeButton(buttonHolder, "prev", "Previous usage");
 
             // Toggle through list of examples in a given file
+            /** @type {function(function(): void): void} */
             const onChangeLoc = changeIndex => {
                 removeClass(highlights[locIndex], "focus");
                 changeIndex();
@@ -106,10 +120,19 @@
         }
     };
 
+    /**
+     * Initialize the `locs` field
+     *
+     * @param {HTMLElement & {locs?: rustdoc.ScrapedLoc[]}} example
+     * @param {boolean} isHidden
+     */
     function setupLoc(example, isHidden) {
-        example.locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent);
+        const locs_str = nonnull(example.attributes.getNamedItem("data-locs")).textContent;
+        const locs =
+              JSON.parse(nonnull(nonnull(locs_str)));
+        example.locs = locs;
         // Start with the first example in view
-        scrollToLoc(example, example.locs[0][0], isHidden);
+        scrollToLoc(example, locs[0][0], isHidden);
     }
 
     const firstExamples = document.querySelectorAll(".scraped-example-list > .scraped-example");
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 10e01b4e262..3fb4db3a89c 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -1,9 +1,16 @@
 // ignore-tidy-filelength
-/* global addClass, getNakedUrl, getSettingValue, getVar */
-/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */
+/* global addClass, getNakedUrl, getVar, nonnull, getSettingValue */
+/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi */
 
 "use strict";
 
+/**
+ * @param {stringdex.Stringdex} Stringdex
+ * @param {typeof stringdex.RoaringBitmap} RoaringBitmap
+ * @param {stringdex.Hooks} hooks
+ */
+const initSearch = async function(Stringdex, RoaringBitmap, hooks) {
+
 // polyfill
 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced
 if (!Array.prototype.toSpliced) {
@@ -20,31 +27,65 @@ if (!Array.prototype.toSpliced) {
  *
  * @template T
  * @param {Iterable<T>} arr
- * @param {function(T): any} func
+ * @param {function(T): Promise<any>} func
  * @param {function(T): boolean} funcBtwn
  */
-function onEachBtwn(arr, func, funcBtwn) {
+async function onEachBtwnAsync(arr, func, funcBtwn) {
     let skipped = true;
     for (const value of arr) {
         if (!skipped) {
             funcBtwn(value);
         }
-        skipped = func(value);
+        skipped = await func(value);
     }
 }
 
 /**
- * Convert any `undefined` to `null`.
- *
- * @template T
- * @param {T|undefined} x
- * @returns {T|null}
+ * Allow the browser to redraw.
+ * @returns {Promise<void>}
  */
-function undef2null(x) {
-    if (x !== undefined) {
-        return x;
-    }
-    return null;
+const yieldToBrowser = typeof window !== "undefined" && window.requestIdleCallback ?
+    function() {
+        return new Promise((resolve, _reject) => {
+            window.requestIdleCallback(resolve);
+        });
+    } :
+    function() {
+        return new Promise((resolve, _reject) => {
+            setTimeout(resolve, 0);
+        });
+    };
+
+/**
+ * Promise-based timer wrapper.
+ * @param {number} ms
+ * @returns {Promise<void>}
+ */
+const timeout = function(ms) {
+    return new Promise((resolve, _reject) => {
+        setTimeout(resolve, ms);
+    });
+};
+
+if (!Promise.withResolvers) {
+    /**
+     * Polyfill
+     * @template T
+     * @returns {{
+            "promise": Promise<T>,
+            "resolve": (function(T): void),
+            "reject": (function(any): void)
+        }}
+     */
+    Promise.withResolvers = () => {
+        let resolve, reject;
+        const promise = new Promise((res, rej) => {
+          resolve = res;
+          reject = rej;
+        });
+        // @ts-expect-error
+        return {promise, resolve, reject};
+    };
 }
 
 // ==================== Core search logic begin ====================
@@ -81,13 +122,22 @@ const itemTypes = [
 ];
 
 // used for special search precedence
-const TY_PRIMITIVE = itemTypes.indexOf("primitive");
-const TY_GENERIC = itemTypes.indexOf("generic");
-const TY_IMPORT = itemTypes.indexOf("import");
-const TY_TRAIT = itemTypes.indexOf("trait");
-const TY_FN = itemTypes.indexOf("fn");
-const TY_METHOD = itemTypes.indexOf("method");
-const TY_TYMETHOD = itemTypes.indexOf("tymethod");
+/** @type {rustdoc.ItemType} */
+const TY_PRIMITIVE = 1;
+/** @type {rustdoc.ItemType} */
+const TY_GENERIC = 26;
+/** @type {rustdoc.ItemType} */
+const TY_IMPORT = 4;
+/** @type {rustdoc.ItemType} */
+const TY_TRAIT = 10;
+/** @type {rustdoc.ItemType} */
+const TY_FN = 7;
+/** @type {rustdoc.ItemType} */
+const TY_METHOD = 13;
+/** @type {rustdoc.ItemType} */
+const TY_TYMETHOD = 12;
+/** @type {rustdoc.ItemType} */
+const TY_ASSOCTYPE = 17;
 const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../";
 
 // Hard limit on how deep to recurse into generics when doing type-driven search.
@@ -242,7 +292,9 @@ function isEndCharacter(c) {
 }
 
 /**
- * @param {number} ty
+ * Same thing as ItemType::is_fn_like in item_type.rs
+ *
+ * @param {rustdoc.ItemType} ty
  * @returns
  */
 function isFnLikeTy(ty) {
@@ -535,6 +587,45 @@ function getNextElem(query, parserState, elems, isInGenerics) {
     /** @type {rustdoc.ParserQueryElement[]} */
     const generics = [];
 
+    /** @type {function(string, string): void} */
+    const handleRefOrPtr = (chr, name) => {
+            if (parserState.typeFilter !== null && parserState.typeFilter !== "primitive") {
+            throw [
+                "Invalid search type: primitive ",
+                chr,
+                " and ",
+                parserState.typeFilter,
+                " both specified",
+            ];
+        }
+        parserState.typeFilter = null;
+        parserState.pos += 1;
+        let c = parserState.userQuery[parserState.pos];
+        while (c === " " && parserState.pos < parserState.length) {
+            parserState.pos += 1;
+            c = parserState.userQuery[parserState.pos];
+        }
+        const generics = [];
+        const pos = parserState.pos;
+        if (parserState.userQuery.slice(pos, pos + 3) === "mut") {
+            generics.push(makePrimitiveElement("mut", { typeFilter: "keyword" }));
+            parserState.pos += 3;
+            c = parserState.userQuery[parserState.pos];
+        } else if (chr === "*" && parserState.userQuery.slice(pos, pos + 5) === "const") {
+            // make *const T parse the same as *T
+            parserState.pos += 5;
+            c = parserState.userQuery[parserState.pos];
+        }
+        while (c === " " && parserState.pos < parserState.length) {
+            parserState.pos += 1;
+            c = parserState.userQuery[parserState.pos];
+        }
+        if (!isEndCharacter(c) && parserState.pos < parserState.length) {
+            getFilteredNextElem(query, parserState, generics, isInGenerics);
+        }
+        elems.push(makePrimitiveElement(name, { generics }));
+    };
+
     skipWhitespace(parserState);
     let start = parserState.pos;
     let end;
@@ -584,36 +675,9 @@ function getNextElem(query, parserState, elems, isInGenerics) {
             elems.push(makePrimitiveElement(name, { bindingName, generics }));
         }
     } else if (parserState.userQuery[parserState.pos] === "&") {
-        if (parserState.typeFilter !== null && parserState.typeFilter !== "primitive") {
-            throw [
-                "Invalid search type: primitive ",
-                "&",
-                " and ",
-                parserState.typeFilter,
-                " both specified",
-            ];
-        }
-        parserState.typeFilter = null;
-        parserState.pos += 1;
-        let c = parserState.userQuery[parserState.pos];
-        while (c === " " && parserState.pos < parserState.length) {
-            parserState.pos += 1;
-            c = parserState.userQuery[parserState.pos];
-        }
-        const generics = [];
-        if (parserState.userQuery.slice(parserState.pos, parserState.pos + 3) === "mut") {
-            generics.push(makePrimitiveElement("mut", { typeFilter: "keyword" }));
-            parserState.pos += 3;
-            c = parserState.userQuery[parserState.pos];
-        }
-        while (c === " " && parserState.pos < parserState.length) {
-            parserState.pos += 1;
-            c = parserState.userQuery[parserState.pos];
-        }
-        if (!isEndCharacter(c) && parserState.pos < parserState.length) {
-            getFilteredNextElem(query, parserState, generics, isInGenerics);
-        }
-        elems.push(makePrimitiveElement("reference", { generics }));
+        handleRefOrPtr("&", "reference");
+    } else if (parserState.userQuery[parserState.pos] === "*") {
+        handleRefOrPtr("*", "pointer");
     } else {
         const isStringElem = parserState.userQuery[start] === "\"";
         // We handle the strings on their own mostly to make code easier to follow.
@@ -1023,6 +1087,7 @@ class VlqHexDecoder {
         this.string = string;
         this.cons = cons;
         this.offset = 0;
+        this.elemCount = 0;
         /** @type {T[]} */
         this.backrefQueue = [];
     }
@@ -1060,6 +1125,7 @@ class VlqHexDecoder {
         n = (n << 4) | (c & 0xF);
         const [sign, value] = [n & 1, n >> 1];
         this.offset += 1;
+        this.elemCount += 1;
         return sign ? -value : value;
     }
     /**
@@ -1086,1253 +1152,142 @@ class VlqHexDecoder {
         return result;
     }
 }
-class RoaringBitmap {
-    /** @param {string} str */
-    constructor(str) {
-        // https://github.com/RoaringBitmap/RoaringFormatSpec
-        //
-        // Roaring bitmaps are used for flags that can be kept in their
-        // compressed form, even when loaded into memory. This decoder
-        // turns the containers into objects, but uses byte array
-        // slices of the original format for the data payload.
-        const strdecoded = atob(str);
-        const u8array = new Uint8Array(strdecoded.length);
-        for (let j = 0; j < strdecoded.length; ++j) {
-            u8array[j] = strdecoded.charCodeAt(j);
-        }
-        const has_runs = u8array[0] === 0x3b;
-        const size = has_runs ?
-            ((u8array[2] | (u8array[3] << 8)) + 1) :
-            ((u8array[4] | (u8array[5] << 8) | (u8array[6] << 16) | (u8array[7] << 24)));
-        let i = has_runs ? 4 : 8;
-        let is_run;
-        if (has_runs) {
-            const is_run_len = Math.floor((size + 7) / 8);
-            is_run = u8array.slice(i, i + is_run_len);
-            i += is_run_len;
-        } else {
-            is_run = new Uint8Array();
-        }
-        this.keys = [];
-        this.cardinalities = [];
-        for (let j = 0; j < size; ++j) {
-            this.keys.push(u8array[i] | (u8array[i + 1] << 8));
-            i += 2;
-            this.cardinalities.push((u8array[i] | (u8array[i + 1] << 8)) + 1);
-            i += 2;
-        }
-        this.containers = [];
-        let offsets = null;
-        if (!has_runs || this.keys.length >= 4) {
-            offsets = [];
-            for (let j = 0; j < size; ++j) {
-                offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) |
-                    (u8array[i + 3] << 24));
-                i += 4;
-            }
-        }
-        for (let j = 0; j < size; ++j) {
-            if (offsets && offsets[j] !== i) {
-                // eslint-disable-next-line no-console
-                console.log(this.containers);
-                throw new Error(`corrupt bitmap ${j}: ${i} / ${offsets[j]}`);
-            }
-            if (is_run[j >> 3] & (1 << (j & 0x7))) {
-                const runcount = (u8array[i] | (u8array[i + 1] << 8));
-                i += 2;
-                this.containers.push(new RoaringBitmapRun(
-                    runcount,
-                    u8array.slice(i, i + (runcount * 4)),
-                ));
-                i += runcount * 4;
-            } else if (this.cardinalities[j] >= 4096) {
-                this.containers.push(new RoaringBitmapBits(u8array.slice(i, i + 8192)));
-                i += 8192;
-            } else {
-                const end = this.cardinalities[j] * 2;
-                this.containers.push(new RoaringBitmapArray(
-                    this.cardinalities[j],
-                    u8array.slice(i, i + end),
-                ));
-                i += end;
-            }
-        }
-    }
-    /** @param {number} keyvalue */
-    contains(keyvalue) {
-        const key = keyvalue >> 16;
-        const value = keyvalue & 0xFFFF;
-        // Binary search algorithm copied from
-        // https://en.wikipedia.org/wiki/Binary_search#Procedure
-        //
-        // Format is required by specification to be sorted.
-        // Because keys are 16 bits and unique, length can't be
-        // bigger than 2**16, and because we have 32 bits of safe int,
-        // left + right can't overflow.
-        let left = 0;
-        let right = this.keys.length - 1;
-        while (left <= right) {
-            const mid = Math.floor((left + right) / 2);
-            const x = this.keys[mid];
-            if (x < key) {
-                left = mid + 1;
-            } else if (x > key) {
-                right = mid - 1;
-            } else {
-                return this.containers[mid].contains(value);
-            }
-        }
-        return false;
-    }
-}
 
-class RoaringBitmapRun {
-    /**
-     * @param {number} runcount
-     * @param {Uint8Array} array
-     */
-    constructor(runcount, array) {
-        this.runcount = runcount;
-        this.array = array;
-    }
-    /** @param {number} value */
-    contains(value) {
-        // Binary search algorithm copied from
-        // https://en.wikipedia.org/wiki/Binary_search#Procedure
-        //
-        // Since runcount is stored as 16 bits, left + right
-        // can't overflow.
-        let left = 0;
-        let right = this.runcount - 1;
-        while (left <= right) {
-            const mid = Math.floor((left + right) / 2);
-            const i = mid * 4;
-            const start = this.array[i] | (this.array[i + 1] << 8);
-            const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8);
-            if ((start + lenm1) < value) {
-                left = mid + 1;
-            } else if (start > value) {
-                right = mid - 1;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    }
-}
-class RoaringBitmapArray {
-    /**
-     * @param {number} cardinality
-     * @param {Uint8Array} array
-     */
-    constructor(cardinality, array) {
-        this.cardinality = cardinality;
-        this.array = array;
-    }
-    /** @param {number} value */
-    contains(value) {
-        // Binary search algorithm copied from
-        // https://en.wikipedia.org/wiki/Binary_search#Procedure
-        //
-        // Since cardinality can't be higher than 4096, left + right
-        // cannot overflow.
-        let left = 0;
-        let right = this.cardinality - 1;
-        while (left <= right) {
-            const mid = Math.floor((left + right) / 2);
-            const i = mid * 2;
-            const x = this.array[i] | (this.array[i + 1] << 8);
-            if (x < value) {
-                left = mid + 1;
-            } else if (x > value) {
-                right = mid - 1;
-            } else {
-                return true;
-            }
-        }
-        return false;
-    }
-}
-class RoaringBitmapBits {
-    /**
-     * @param {Uint8Array} array
-     */
-    constructor(array) {
-        this.array = array;
-    }
-    /** @param {number} value */
-    contains(value) {
-        return !!(this.array[value >> 3] & (1 << (value & 7)));
-    }
-}
+/** @type {Array<string>} */
+const EMPTY_STRING_ARRAY = [];
+
+/** @type {Array<rustdoc.FunctionType>} */
+const EMPTY_GENERICS_ARRAY = [];
+
+/** @type {Array<[number, rustdoc.FunctionType[]]>} */
+const EMPTY_BINDINGS_ARRAY = [];
+
+/** @type {Map<number, Array<any>>} */
+const EMPTY_BINDINGS_MAP = new Map();
 
 /**
- * A prefix tree, used for name-based search.
- *
- * This data structure is used to drive prefix matches,
- * such as matching the query "link" to `LinkedList`,
- * and Lev-distance matches, such as matching the
- * query "hahsmap" to `HashMap`. Substring matches,
- * such as "list" to `LinkedList`, are done with a
- * tailTable that deep-links into this trie.
- *
- * children
- * : A [sparse array] of subtrees. The array index
- *   is a charCode.
- *
- *   [sparse array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/
- *     Indexed_collections#sparse_arrays
- *
- * matches
- * : A list of search index IDs for this node.
- *
- * @type {{
- *     children: NameTrie[],
- *     matches: number[],
- * }}
+ * @param {string|null} typename
+ * @returns {number}
  */
-class NameTrie {
-    constructor() {
-        this.children = [];
-        this.matches = [];
-    }
-    /**
-     * @param {string} name
-     * @param {number} id
-     * @param {Map<string, NameTrie[]>} tailTable
-     */
-    insert(name, id, tailTable) {
-        this.insertSubstring(name, 0, id, tailTable);
-    }
-    /**
-     * @param {string} name
-     * @param {number} substart
-     * @param {number} id
-     * @param {Map<string, NameTrie[]>} tailTable
-     */
-    insertSubstring(name, substart, id, tailTable) {
-        const l = name.length;
-        if (substart === l) {
-            this.matches.push(id);
-        } else {
-            const sb = name.charCodeAt(substart);
-            let child;
-            if (this.children[sb] !== undefined) {
-                child = this.children[sb];
-            } else {
-                child = new NameTrie();
-                this.children[sb] = child;
-                /** @type {NameTrie[]} */
-                let sste;
-                if (substart >= 2) {
-                    const tail = name.substring(substart - 2, substart + 1);
-                    const entry = tailTable.get(tail);
-                    if (entry !== undefined) {
-                        sste = entry;
-                    } else {
-                        sste = [];
-                        tailTable.set(tail, sste);
-                    }
-                    sste.push(child);
-                }
-            }
-            child.insertSubstring(name, substart + 1, id, tailTable);
-        }
-    }
-    /**
-     * @param {string} name
-     * @param {Map<string, NameTrie[]>} tailTable
-     */
-    search(name, tailTable) {
-        const results = new Set();
-        this.searchSubstringPrefix(name, 0, results);
-        if (results.size < MAX_RESULTS && name.length >= 3) {
-            const levParams = name.length >= 6 ?
-                new Lev2TParametricDescription(name.length) :
-                new Lev1TParametricDescription(name.length);
-            this.searchLev(name, 0, levParams, results);
-            const tail = name.substring(0, 3);
-            const list = tailTable.get(tail);
-            if (list !== undefined) {
-                for (const entry of list) {
-                    entry.searchSubstringPrefix(name, 3, results);
-                }
-            }
-        }
-        return [...results];
+function itemTypeFromName(typename) {
+    if (typename === null) {
+        return NO_TYPE_FILTER;
     }
-    /**
-     * @param {string} name
-     * @param {number} substart
-     * @param {Set<number>} results
-     */
-    searchSubstringPrefix(name, substart, results) {
-        const l = name.length;
-        if (substart === l) {
-            for (const match of this.matches) {
-                results.add(match);
-            }
-            // breadth-first traversal orders prefix matches by length
-            /** @type {NameTrie[]} */
-            let unprocessedChildren = [];
-            for (const child of this.children) {
-                if (child) {
-                    unprocessedChildren.push(child);
-                }
-            }
-            /** @type {NameTrie[]} */
-            let nextSet = [];
-            while (unprocessedChildren.length !== 0) {
-                /** @type {NameTrie} */
-                // @ts-expect-error
-                const next = unprocessedChildren.pop();
-                for (const child of next.children) {
-                    if (child) {
-                        nextSet.push(child);
-                    }
-                }
-                for (const match of next.matches) {
-                    results.add(match);
-                }
-                if (unprocessedChildren.length === 0) {
-                    const tmp = unprocessedChildren;
-                    unprocessedChildren = nextSet;
-                    nextSet = tmp;
-                }
-            }
-        } else {
-            const sb = name.charCodeAt(substart);
-            if (this.children[sb] !== undefined) {
-                this.children[sb].searchSubstringPrefix(name, substart + 1, results);
-            }
-        }
-    }
-    /**
-     * @param {string} name
-     * @param {number} substart
-     * @param {Lev2TParametricDescription|Lev1TParametricDescription} levParams
-     * @param {Set<number>} results
-     */
-    searchLev(name, substart, levParams, results) {
-        const stack = [[this, 0]];
-        const n = levParams.n;
-        while (stack.length !== 0) {
-            // It's not empty
-            //@ts-expect-error
-            const [trie, levState] = stack.pop();
-            for (const [charCode, child] of trie.children.entries()) {
-                if (!child) {
-                    continue;
-                }
-                const levPos = levParams.getPosition(levState);
-                const vector = levParams.getVector(
-                    name,
-                    charCode,
-                    levPos,
-                    Math.min(name.length, levPos + (2 * n) + 1),
-                );
-                const newLevState = levParams.transition(
-                    levState,
-                    levPos,
-                    vector,
-                );
-                if (newLevState >= 0) {
-                    stack.push([child, newLevState]);
-                    if (levParams.isAccept(newLevState)) {
-                        for (const match of child.matches) {
-                            results.add(match);
-                        }
-                    }
-                }
-            }
-        }
+    const index = itemTypes.findIndex(i => i === typename);
+    if (index < 0) {
+        throw ["Unknown type filter ", typename];
     }
+    return index;
 }
 
 class DocSearch {
     /**
-     * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex
      * @param {string} rootPath
-     * @param {rustdoc.SearchState} searchState
+     * @param {stringdex.Database} database
      */
-    constructor(rawSearchIndex, rootPath, searchState) {
-        /**
-         * @type {Map<String, RoaringBitmap>}
-         */
-        this.searchIndexDeprecated = new Map();
-        /**
-         * @type {Map<String, RoaringBitmap>}
-         */
-        this.searchIndexEmptyDesc = new Map();
-        /**
-         * @type {Map<String, RoaringBitmap>}
-         */
-        this.searchIndexUnstable = new Map();
-
-        /**
-         *  @type {Uint32Array}
-         */
-        this.functionTypeFingerprint = new Uint32Array(0);
-        /**
-         * Map from normalized type names to integers. Used to make type search
-         * more efficient.
-         *
-         * @type {Map<string, {id: number, assocOnly: boolean}>}
-         */
-        this.typeNameIdMap = new Map();
-        /**
-         * Map from type ID to associated type name. Used for display,
-         * not for search.
-         *
-         * @type {Map<number, string>}
-         */
-        this.assocTypeIdNameMap = new Map();
-        this.ALIASES = new Map();
-        this.FOUND_ALIASES = new Set();
+    constructor(rootPath, database) {
         this.rootPath = rootPath;
-        this.searchState = searchState;
-
-        /**
-         * Special type name IDs for searching by array.
-         * @type {number}
-         */
-        this.typeNameIdOfArray = this.buildTypeMapIndex("array");
-        /**
-         * Special type name IDs for searching by slice.
-         * @type {number}
-         */
-        this.typeNameIdOfSlice = this.buildTypeMapIndex("slice");
-        /**
-         * Special type name IDs for searching by both array and slice (`[]` syntax).
-         * @type {number}
-         */
-        this.typeNameIdOfArrayOrSlice = this.buildTypeMapIndex("[]");
-        /**
-         * Special type name IDs for searching by tuple.
-         * @type {number}
-         */
-        this.typeNameIdOfTuple = this.buildTypeMapIndex("tuple");
-        /**
-         * Special type name IDs for searching by unit.
-         * @type {number}
-         */
-        this.typeNameIdOfUnit = this.buildTypeMapIndex("unit");
-        /**
-         * Special type name IDs for searching by both tuple and unit (`()` syntax).
-         * @type {number}
-         */
-        this.typeNameIdOfTupleOrUnit = this.buildTypeMapIndex("()");
-        /**
-         * Special type name IDs for searching `fn`.
-         * @type {number}
-         */
-        this.typeNameIdOfFn = this.buildTypeMapIndex("fn");
-        /**
-         * Special type name IDs for searching `fnmut`.
-         * @type {number}
-         */
-        this.typeNameIdOfFnMut = this.buildTypeMapIndex("fnmut");
-        /**
-         * Special type name IDs for searching `fnonce`.
-         * @type {number}
-         */
-        this.typeNameIdOfFnOnce = this.buildTypeMapIndex("fnonce");
-        /**
-         * Special type name IDs for searching higher order functions (`->` syntax).
-         * @type {number}
-         */
-        this.typeNameIdOfHof = this.buildTypeMapIndex("->");
-        /**
-         * Special type name IDs the output assoc type.
-         * @type {number}
-         */
-        this.typeNameIdOfOutput = this.buildTypeMapIndex("output", true);
-        /**
-         * Special type name IDs for searching by reference.
-         * @type {number}
-         */
-        this.typeNameIdOfReference = this.buildTypeMapIndex("reference");
-
-        /**
-         * Empty, immutable map used in item search types with no bindings.
-         *
-         * @type {Map<number, Array<any>>}
-         */
-        this.EMPTY_BINDINGS_MAP = new Map();
-
-        /**
-         * Empty, immutable map used in item search types with no bindings.
-         *
-         * @type {Array<any>}
-         */
-        this.EMPTY_GENERICS_ARRAY = [];
-
-        /**
-         * Object pool for function types with no bindings or generics.
-         * This is reset after loading the index.
-         *
-         * @type {Map<number|null, rustdoc.FunctionType>}
-         */
+        this.database = database;
+
+        this.typeNameIdOfOutput = -1;
+        this.typeNameIdOfArray = -1;
+        this.typeNameIdOfSlice = -1;
+        this.typeNameIdOfArrayOrSlice = -1;
+        this.typeNameIdOfTuple = -1;
+        this.typeNameIdOfUnit = -1;
+        this.typeNameIdOfTupleOrUnit = -1;
+        this.typeNameIdOfReference = -1;
+        this.typeNameIdOfPointer = -1;
+        this.typeNameIdOfHof = -1;
+
+        this.utf8decoder = new TextDecoder();
+
+        /** @type {Map<number|null, rustdoc.FunctionType>} */
         this.TYPES_POOL = new Map();
-
-        /**
-         * A trie for finding items by name.
-         * This is used for edit distance and prefix finding.
-         *
-         * @type {NameTrie}
-         */
-        this.nameTrie = new NameTrie();
-
-        /**
-         * Find items by 3-substring. This is a map from three-char
-         * prefixes into lists of subtries.
-         */
-        this.tailTable = new Map();
-
-        /**
-         *  @type {Array<rustdoc.Row>}
-         */
-        this.searchIndex = this.buildIndex(rawSearchIndex);
     }
 
     /**
-     * Add an item to the type Name->ID map, or, if one already exists, use it.
-     * Returns the number. If name is "" or null, return null (pure generic).
-     *
-     * This is effectively string interning, so that function matching can be
-     * done more quickly. Two types with the same name but different item kinds
-     * get the same ID.
-     *
-     * @template T extends string
-     * @overload
-     * @param {T} name
-     * @param {boolean=} isAssocType - True if this is an assoc type
-     * @returns {T extends "" ? null : number}
-     *
-     * @param {string} name
-     * @param {boolean=} isAssocType
-     * @returns {number | null}
-     *
+     * Load search index. If you do not call this function, `execQuery`
+     * will never fulfill.
      */
-    buildTypeMapIndex(name, isAssocType) {
-        if (name === "" || name === null) {
-            return null;
-        }
-
-        const obj = this.typeNameIdMap.get(name);
-        if (obj !== undefined) {
-            obj.assocOnly = !!(isAssocType && obj.assocOnly);
-            return obj.id;
-        } else {
-            const id = this.typeNameIdMap.size;
-            this.typeNameIdMap.set(name, { id, assocOnly: !!isAssocType });
-            return id;
-        }
-    }
-
-    /**
-     * Convert a list of RawFunctionType / ID to object-based FunctionType.
-     *
-     * Crates often have lots of functions in them, and it's common to have a large number of
-     * functions that operate on a small set of data types, so the search index compresses them
-     * by encoding function parameter and return types as indexes into an array of names.
-     *
-     * Even when a general-purpose compression algorithm is used, this is still a win.
-     * I checked. https://github.com/rust-lang/rust/pull/98475#issue-1284395985
-     *
-     * The format for individual function types is encoded in
-     * librustdoc/html/render/mod.rs: impl Serialize for RenderType
-     *
-     * @param {null|Array<rustdoc.RawFunctionType>} types
-     * @param {Array<{
-     *     name: string,
-    *     ty: number,
-    *     path: string|null,
-    *     exactPath: string|null,
-    *     unboxFlag: boolean
-    * }>} paths
-    * @param {Array<{
-    *     name: string,
-    *     ty: number,
-    *     path: string|null,
-    *     exactPath: string|null,
-    *     unboxFlag: boolean,
-    * }>} lowercasePaths
-     *
-     * @return {Array<rustdoc.FunctionType>}
-     */
-    buildItemSearchTypeAll(types, paths, lowercasePaths) {
-        return types && types.length > 0 ?
-            types.map(type => this.buildItemSearchType(type, paths, lowercasePaths)) :
-            this.EMPTY_GENERICS_ARRAY;
-    }
-
-    /**
-     * Converts a single type.
-     *
-     * @param {rustdoc.RawFunctionType} type
-     * @param {Array<{
-     *     name: string,
-     *     ty: number,
-     *     path: string|null,
-     *     exactPath: string|null,
-     *     unboxFlag: boolean
-     * }>} paths
-     * @param {Array<{
-     *     name: string,
-     *     ty: number,
-     *     path: string|null,
-     *     exactPath: string|null,
-     *     unboxFlag: boolean,
-     * }>} lowercasePaths
-     * @param {boolean=} isAssocType
-     */
-    buildItemSearchType(type, paths, lowercasePaths, isAssocType) {
-        const PATH_INDEX_DATA = 0;
-        const GENERICS_DATA = 1;
-        const BINDINGS_DATA = 2;
-        let pathIndex, generics, bindings;
-        if (typeof type === "number") {
-            pathIndex = type;
-            generics = this.EMPTY_GENERICS_ARRAY;
-            bindings = this.EMPTY_BINDINGS_MAP;
-        } else {
-            pathIndex = type[PATH_INDEX_DATA];
-            generics = this.buildItemSearchTypeAll(
-                type[GENERICS_DATA],
-                paths,
-                lowercasePaths,
-            );
-            // @ts-expect-error
-            if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) {
-                // @ts-expect-error
-                bindings = new Map(type[BINDINGS_DATA].map(binding => {
-                    const [assocType, constraints] = binding;
-                    // Associated type constructors are represented sloppily in rustdoc's
-                    // type search, to make the engine simpler.
-                    //
-                    // MyType<Output<T>=Result<T>> is equivalent to MyType<Output<Result<T>>=T>
-                    // and both are, essentially
-                    // MyType<Output=(T, Result<T>)>, except the tuple isn't actually there.
-                    // It's more like the value of a type binding is naturally an array,
-                    // which rustdoc calls "constraints".
-                    //
-                    // As a result, the key should never have generics on it.
-                    return [
-                        this.buildItemSearchType(assocType, paths, lowercasePaths, true).id,
-                        this.buildItemSearchTypeAll(constraints, paths, lowercasePaths),
-                    ];
-                }));
-            } else {
-                bindings = this.EMPTY_BINDINGS_MAP;
-            }
+    async buildIndex() {
+        const nn = this.database.getIndex("normalizedName");
+        if (!nn) {
+            return;
         }
+        // Each of these identifiers are used specially by
+        // type-driven search.
+        const [
+            // output is the special associated type that goes
+            // after the arrow: the type checker desugars
+            // the path `Fn(a) -> b` into `Fn<Output=b, (a)>`
+            output,
+            // fn, fnmut, and fnonce all match `->`
+            fn,
+            fnMut,
+            fnOnce,
+            hof,
+            // array and slice both match `[]`
+            array,
+            slice,
+            arrayOrSlice,
+            // tuple and unit both match `()`
+            tuple,
+            unit,
+            tupleOrUnit,
+            // reference matches `&`
+            reference,
+            pointer,
+            // never matches `!`
+            never,
+        ] = await Promise.all([
+            nn.search("output"),
+            nn.search("fn"),
+            nn.search("fnmut"),
+            nn.search("fnonce"),
+            nn.search("->"),
+            nn.search("array"),
+            nn.search("slice"),
+            nn.search("[]"),
+            nn.search("tuple"),
+            nn.search("unit"),
+            nn.search("()"),
+            nn.search("reference"),
+            nn.search("pointer"),
+            nn.search("never"),
+        ]);
         /**
-         * @type {rustdoc.FunctionType}
-         */
-        let result;
-        if (pathIndex < 0) {
-            // types less than 0 are generic parameters
-            // the actual names of generic parameters aren't stored, since they aren't API
-            result = {
-                id: pathIndex,
-                name: "",
-                ty: TY_GENERIC,
-                path: null,
-                exactPath: null,
-                generics,
-                bindings,
-                unboxFlag: true,
-            };
-        } else if (pathIndex === 0) {
-            // `0` is used as a sentinel because it's fewer bytes than `null`
-            result = {
-                id: null,
-                name: "",
-                ty: null,
-                path: null,
-                exactPath: null,
-                generics,
-                bindings,
-                unboxFlag: true,
-            };
-        } else {
-            const item = lowercasePaths[pathIndex - 1];
-            const id = this.buildTypeMapIndex(item.name, isAssocType);
-            if (isAssocType && id !== null) {
-                this.assocTypeIdNameMap.set(id, paths[pathIndex - 1].name);
-            }
-            result = {
-                id,
-                name: paths[pathIndex - 1].name,
-                ty: item.ty,
-                path: item.path,
-                exactPath: item.exactPath,
-                generics,
-                bindings,
-                unboxFlag: item.unboxFlag,
-            };
-        }
-        const cr = this.TYPES_POOL.get(result.id);
-        if (cr) {
-            // Shallow equality check. Since this function is used
-            // to construct every type object, this should be mostly
-            // equivalent to a deep equality check, except if there's
-            // a conflict, we don't keep the old one around, so it's
-            // not a fully precise implementation of hashcons.
-            if (cr.generics.length === result.generics.length &&
-                cr.generics !== result.generics &&
-                cr.generics.every((x, i) => result.generics[i] === x)
-            ) {
-                result.generics = cr.generics;
-            }
-            if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) {
-                let ok = true;
-                for (const [k, v] of cr.bindings.entries()) {
-                    // @ts-expect-error
-                    const v2 = result.bindings.get(v);
-                    if (!v2) {
-                        ok = false;
-                        break;
+         * @param {stringdex.Trie|null|undefined} trie
+         * @param {rustdoc.ItemType} ty
+         * @param {string} modulePath
+         * @returns {Promise<number>}
+         * */
+        const first = async(trie, ty, modulePath) => {
+            if (trie) {
+                for (const id of trie.matches().entries()) {
+                    const pathData = await this.getPathData(id);
+                    if (pathData && pathData.ty === ty && pathData.modulePath === modulePath) {
+                        return id;
                     }
-                    if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) {
-                        result.bindings.set(k, v);
-                    } else if (v !== v2) {
-                        ok = false;
-                        break;
-                    }
-                }
-                if (ok) {
-                    result.bindings = cr.bindings;
                 }
             }
-            if (cr.ty === result.ty && cr.path === result.path
-                && cr.bindings === result.bindings && cr.generics === result.generics
-                && cr.ty === result.ty && cr.name === result.name
-                && cr.unboxFlag === result.unboxFlag
-            ) {
-                return cr;
-            }
-        }
-        this.TYPES_POOL.set(result.id, result);
-        return result;
-    }
-
-    /**
-     * Type fingerprints allow fast, approximate matching of types.
-     *
-     * This algo creates a compact representation of the type set using a Bloom filter.
-     * This fingerprint is used three ways:
-     *
-     * - It accelerates the matching algorithm by checking the function fingerprint against the
-     *   query fingerprint. If any bits are set in the query but not in the function, it can't
-     *   match.
-     *
-     * - The fourth section has the number of items in the set.
-     *   This is the distance function, used for filtering and for sorting.
-     *
-     * [^1]: Distance is the relatively naive metric of counting the number of distinct items in
-     * the function that are not present in the query.
-     *
-     * @param {rustdoc.FingerprintableType} type - a single type
-     * @param {Uint32Array} output - write the fingerprint to this data structure: uses 128 bits
-     */
-    buildFunctionTypeFingerprint(type, output) {
-        let input = type.id;
-        // All forms of `[]`/`()`/`->` get collapsed down to one thing in the bloom filter.
-        // Differentiating between arrays and slices, if the user asks for it, is
-        // still done in the matching algorithm.
-        if (input === this.typeNameIdOfArray || input === this.typeNameIdOfSlice) {
-            input = this.typeNameIdOfArrayOrSlice;
-        }
-        if (input === this.typeNameIdOfTuple || input === this.typeNameIdOfUnit) {
-            input = this.typeNameIdOfTupleOrUnit;
-        }
-        if (input === this.typeNameIdOfFn || input === this.typeNameIdOfFnMut ||
-            input === this.typeNameIdOfFnOnce) {
-            input = this.typeNameIdOfHof;
-        }
-        /**
-         * http://burtleburtle.net/bob/hash/integer.html
-         * ~~ is toInt32. It's used before adding, so
-         * the number stays in safe integer range.
-         * @param {number} k
-         */
-        const hashint1 = k => {
-            k = (~~k + 0x7ed55d16) + (k << 12);
-            k = (k ^ 0xc761c23c) ^ (k >>> 19);
-            k = (~~k + 0x165667b1) + (k << 5);
-            k = (~~k + 0xd3a2646c) ^ (k << 9);
-            k = (~~k + 0xfd7046c5) + (k << 3);
-            return (k ^ 0xb55a4f09) ^ (k >>> 16);
-        };
-        /** @param {number} k */
-        const hashint2 = k => {
-            k = ~k + (k << 15);
-            k ^= k >>> 12;
-            k += k << 2;
-            k ^= k >>> 4;
-            k = Math.imul(k, 2057);
-            return k ^ (k >> 16);
-        };
-        if (input !== null) {
-            const h0a = hashint1(input);
-            const h0b = hashint2(input);
-            // Less Hashing, Same Performance: Building a Better Bloom Filter
-            // doi=10.1.1.72.2442
-            const h1a = ~~(h0a + Math.imul(h0b, 2));
-            const h1b = ~~(h0a + Math.imul(h0b, 3));
-            const h2a = ~~(h0a + Math.imul(h0b, 4));
-            const h2b = ~~(h0a + Math.imul(h0b, 5));
-            output[0] |= (1 << (h0a % 32)) | (1 << (h1b % 32));
-            output[1] |= (1 << (h1a % 32)) | (1 << (h2b % 32));
-            output[2] |= (1 << (h2a % 32)) | (1 << (h0b % 32));
-            // output[3] is the total number of items in the type signature
-            output[3] += 1;
-        }
-        for (const g of type.generics) {
-            this.buildFunctionTypeFingerprint(g, output);
-        }
-        /**
-         * @type {{
-         *   id: number|null,
-         *   ty: number,
-         *   generics: rustdoc.FingerprintableType[],
-         *   bindings: Map<number, rustdoc.FingerprintableType[]>
-         * }}
-         */
-        const fb = {
-            id: null,
-            ty: 0,
-            generics: this.EMPTY_GENERICS_ARRAY,
-            bindings: this.EMPTY_BINDINGS_MAP,
-        };
-        for (const [k, v] of type.bindings.entries()) {
-            fb.id = k;
-            fb.generics = v;
-            this.buildFunctionTypeFingerprint(fb, output);
-        }
-    }
-
-    /**
-     * Convert raw search index into in-memory search index.
-     *
-     * @param {Map<string, rustdoc.RawSearchIndexCrate>} rawSearchIndex
-     * @returns {rustdoc.Row[]}
-     */
-    buildIndex(rawSearchIndex) {
-        /**
-         * Convert from RawFunctionSearchType to FunctionSearchType.
-         *
-         * Crates often have lots of functions in them, and function signatures are sometimes
-         * complex, so rustdoc uses a pretty tight encoding for them. This function converts it
-         * to a simpler, object-based encoding so that the actual search code is more readable
-         * and easier to debug.
-         *
-         * The raw function search type format is generated using serde in
-         * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string
-         *
-         * @param {Array<{
-         *     name: string,
-         *     ty: number,
-         *     path: string|null,
-         *     exactPath: string|null,
-         *     unboxFlag: boolean
-         * }>} paths
-         * @param {Array<{
-         *     name: string,
-         *     ty: number,
-         *     path: string|null,
-         *     exactPath: string|null,
-         *     unboxFlag: boolean
-         * }>} lowercasePaths
-         *
-         * @return {function(rustdoc.RawFunctionSearchType): null|rustdoc.FunctionSearchType}
-         */
-        const buildFunctionSearchTypeCallback = (paths, lowercasePaths) => {
-            /**
-             * @param {rustdoc.RawFunctionSearchType} functionSearchType
-             */
-            const cb = functionSearchType => {
-                if (functionSearchType === 0) {
-                    return null;
-                }
-                const INPUTS_DATA = 0;
-                const OUTPUT_DATA = 1;
-                /** @type {rustdoc.FunctionType[]} */
-                let inputs;
-                /** @type {rustdoc.FunctionType[]} */
-                let output;
-                if (typeof functionSearchType[INPUTS_DATA] === "number") {
-                    inputs = [
-                        this.buildItemSearchType(
-                            functionSearchType[INPUTS_DATA],
-                            paths,
-                            lowercasePaths,
-                        ),
-                    ];
-                } else {
-                    inputs = this.buildItemSearchTypeAll(
-                        functionSearchType[INPUTS_DATA],
-                        paths,
-                        lowercasePaths,
-                    );
-                }
-                if (functionSearchType.length > 1) {
-                    if (typeof functionSearchType[OUTPUT_DATA] === "number") {
-                        output = [
-                            this.buildItemSearchType(
-                                functionSearchType[OUTPUT_DATA],
-                                paths,
-                                lowercasePaths,
-                            ),
-                        ];
-                    } else {
-                        output = this.buildItemSearchTypeAll(
-                            // @ts-expect-error
-                            functionSearchType[OUTPUT_DATA],
-                            paths,
-                            lowercasePaths,
-                        );
-                    }
-                } else {
-                    output = [];
-                }
-                const where_clause = [];
-                const l = functionSearchType.length;
-                for (let i = 2; i < l; ++i) {
-                    where_clause.push(typeof functionSearchType[i] === "number"
-                        // @ts-expect-error
-                        ? [this.buildItemSearchType(functionSearchType[i], paths, lowercasePaths)]
-                        : this.buildItemSearchTypeAll(
-                            // @ts-expect-error
-                            functionSearchType[i],
-                            paths,
-                            lowercasePaths,
-                        ));
-                }
-                return {
-                    inputs, output, where_clause,
-                };
-            };
-            return cb;
+            return -1;
         };
-
-        /** @type {rustdoc.Row[]} */
-        const searchIndex = [];
-        let currentIndex = 0;
-        let id = 0;
-
-        // Function type fingerprints are 128-bit bloom filters that are used to
-        // estimate the distance between function and query.
-        // This loop counts the number of items to allocate a fingerprint for.
-        for (const crate of rawSearchIndex.values()) {
-            // Each item gets an entry in the fingerprint array, and the crate
-            // does, too
-            id += crate.t.length + 1;
-        }
-        this.functionTypeFingerprint = new Uint32Array((id + 1) * 4);
-        // This loop actually generates the search item indexes, including
-        // normalized names, type signature objects and fingerprints, and aliases.
-        id = 0;
-
-        /** @type {Array<[string, { [key: string]: Array<number> },  number]>} */
-        const allAliases = [];
-        for (const [crate, crateCorpus] of rawSearchIndex) {
-            // a string representing the lengths of each description shard
-            // a string representing the list of function types
-            const itemDescShardDecoder = new VlqHexDecoder(crateCorpus.D, noop => {
-                /** @type {number} */
-                // @ts-expect-error
-                const n = noop;
-                return n;
-            });
-            let descShard = {
-                crate,
-                shard: 0,
-                start: 0,
-                len: itemDescShardDecoder.next(),
-                promise: null,
-                resolve: null,
-            };
-            const descShardList = [descShard];
-
-            // Deprecated and unstable items and items with no description
-            this.searchIndexDeprecated.set(crate, new RoaringBitmap(crateCorpus.c));
-            this.searchIndexEmptyDesc.set(crate, new RoaringBitmap(crateCorpus.e));
-            this.searchIndexUnstable.set(crate, new RoaringBitmap(crateCorpus.u));
-            let descIndex = 0;
-
-            /**
-             * List of generic function type parameter names.
-             * Used for display, not for searching.
-             * @type {string[]}
-             */
-            let lastParamNames = [];
-
-            // This object should have exactly the same set of fields as the "row"
-            // object defined below. Your JavaScript runtime will thank you.
-            // https://mathiasbynens.be/notes/shapes-ics
-            let normalizedName = crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, "");
-            const crateRow = {
-                crate,
-                ty: 3, // == ExternCrate
-                name: crate,
-                path: "",
-                descShard,
-                descIndex,
-                exactPath: "",
-                desc: crateCorpus.doc,
-                parent: undefined,
-                type: null,
-                paramNames: lastParamNames,
-                id,
-                word: crate,
-                normalizedName,
-                bitIndex: 0,
-                implDisambiguator: null,
-            };
-            this.nameTrie.insert(normalizedName, id, this.tailTable);
-            id += 1;
-            searchIndex.push(crateRow);
-            currentIndex += 1;
-            // it's not undefined
-            // @ts-expect-error
-            if (!this.searchIndexEmptyDesc.get(crate).contains(0)) {
-                descIndex += 1;
-            }
-
-            // see `RawSearchIndexCrate` in `rustdoc.d.ts` for a more
-            // up to date description of these fields
-            const itemTypes = crateCorpus.t;
-            // an array of (String) item names
-            const itemNames = crateCorpus.n;
-            // an array of [(Number) item index,
-            //              (String) full path]
-            // an item whose index is not present will fall back to the previous present path
-            // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present,
-            // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11
-            const itemPaths = new Map(crateCorpus.q);
-            // An array of [(Number) item index, (Number) path index]
-            // Used to de-duplicate inlined and re-exported stuff
-            const itemReexports = new Map(crateCorpus.r);
-            // an array of (Number) the parent path index + 1 to `paths`, or 0 if none
-            const itemParentIdxDecoder = new VlqHexDecoder(crateCorpus.i, noop => noop);
-            // a map Number, string for impl disambiguators
-            const implDisambiguator = new Map(crateCorpus.b);
-            const rawPaths = crateCorpus.p;
-            const aliases = crateCorpus.a;
-            // an array of [(Number) item index,
-            //              (String) comma-separated list of function generic param names]
-            // an item whose index is not present will fall back to the previous present path
-            const itemParamNames = new Map(crateCorpus.P);
-
-            /**
-             * @type {Array<{
-             *     name: string,
-             *     ty: number,
-             *     path: string|null,
-             *     exactPath: string|null,
-             *     unboxFlag: boolean
-             * }>}
-             */
-            const lowercasePaths = [];
-            /**
-             * @type {Array<{
-             *     name: string,
-             *     ty: number,
-             *     path: string|null,
-             *     exactPath: string|null,
-             *     unboxFlag: boolean
-             * }>}
-             */
-            const paths = [];
-
-            // a string representing the list of function types
-            const itemFunctionDecoder = new VlqHexDecoder(
-                crateCorpus.f,
-                // @ts-expect-error
-                buildFunctionSearchTypeCallback(paths, lowercasePaths),
-            );
-
-            // convert `rawPaths` entries into object form
-            // generate normalizedPaths for function search mode
-            let len = rawPaths.length;
-            let lastPath = undef2null(itemPaths.get(0));
-            for (let i = 0; i < len; ++i) {
-                const elem = rawPaths[i];
-                const ty = elem[0];
-                const name = elem[1];
-                /**
-                 * @param {2|3} idx
-                 * @param {string|null} if_null
-                 * @param {string|null} if_not_found
-                 * @returns {string|null}
-                 */
-                const elemPath = (idx, if_null, if_not_found) => {
-                    if (elem.length > idx && elem[idx] !== undefined) {
-                        const p = itemPaths.get(elem[idx]);
-                        if (p !== undefined) {
-                            return p;
-                        }
-                        return if_not_found;
-                    }
-                    return if_null;
-                };
-                const path = elemPath(2, lastPath, null);
-                const exactPath = elemPath(3, path, path);
-                const unboxFlag = elem.length > 4 && !!elem[4];
-
-                lowercasePaths.push({ ty, name: name.toLowerCase(), path, exactPath, unboxFlag });
-                paths[i] = { ty, name, path, exactPath, unboxFlag };
-            }
-
-            // Convert `item*` into an object form, and construct word indices.
-            //
-            // Before any analysis is performed, let's gather the search terms to
-            // search against apart from the rest of the data. This is a quick
-            // operation that is cached for the life of the page state so that
-            // all other search operations have access to this cached data for
-            // faster analysis operations
-            lastPath = "";
-            len = itemTypes.length;
-            let lastName = "";
-            let lastWord = "";
-            for (let i = 0; i < len; ++i) {
-                const bitIndex = i + 1;
-                if (descIndex >= descShard.len &&
-                    // @ts-expect-error
-                    !this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) {
-                    descShard = {
-                        crate,
-                        shard: descShard.shard + 1,
-                        start: descShard.start + descShard.len,
-                        len: itemDescShardDecoder.next(),
-                        promise: null,
-                        resolve: null,
-                    };
-                    descIndex = 0;
-                    descShardList.push(descShard);
-                }
-                const name = itemNames[i] === "" ? lastName : itemNames[i];
-                const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase();
-                const pathU = itemPaths.get(i);
-                const path = pathU !== undefined ? pathU : lastPath;
-                const paramNameString = itemParamNames.get(i);
-                const paramNames = paramNameString !== undefined ?
-                    paramNameString.split(",") :
-                    lastParamNames;
-                const type = itemFunctionDecoder.next();
-                if (type !== null) {
-                    if (type) {
-                        const fp = this.functionTypeFingerprint.subarray(id * 4, (id + 1) * 4);
-                        for (const t of type.inputs) {
-                            this.buildFunctionTypeFingerprint(t, fp);
-                        }
-                        for (const t of type.output) {
-                            this.buildFunctionTypeFingerprint(t, fp);
-                        }
-                        for (const w of type.where_clause) {
-                            for (const t of w) {
-                                this.buildFunctionTypeFingerprint(t, fp);
-                            }
-                        }
-                    }
-                }
-                // This object should have exactly the same set of fields as the "crateRow"
-                // object defined above.
-                const itemParentIdx = itemParentIdxDecoder.next();
-                normalizedName = word.indexOf("_") === -1 ? word : word.replace(/_/g, "");
-                /** @type {rustdoc.Row} */
-                const row = {
-                    crate,
-                    ty: itemTypes.charCodeAt(i) - 65, // 65 = "A"
-                    name,
-                    path,
-                    descShard,
-                    descIndex,
-                    exactPath: itemReexports.has(i) ?
-                        // @ts-expect-error
-                        itemPaths.get(itemReexports.get(i)) : path,
-                    // @ts-expect-error
-                    parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined,
-                    type,
-                    paramNames,
-                    id,
-                    word,
-                    normalizedName,
-                    bitIndex,
-                    implDisambiguator: undef2null(implDisambiguator.get(i)),
-                };
-                this.nameTrie.insert(normalizedName, id, this.tailTable);
-                id += 1;
-                searchIndex.push(row);
-                lastPath = row.path;
-                lastParamNames = row.paramNames;
-                // @ts-expect-error
-                if (!this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) {
-                    descIndex += 1;
-                }
-                lastName = name;
-                lastWord = word;
-            }
-
-            if (aliases) {
-                // We need to add the aliases in `searchIndex` after we finished filling it
-                // to not mess up indexes.
-                allAliases.push([crate, aliases, currentIndex]);
-            }
-            currentIndex += itemTypes.length;
-            this.searchState.descShards.set(crate, descShardList);
-        }
-
-        for (const [crate, aliases, index] of allAliases) {
-            for (const [alias_name, alias_refs] of Object.entries(aliases)) {
-                if (!this.ALIASES.has(crate)) {
-                    this.ALIASES.set(crate, new Map());
-                }
-                const word = alias_name.toLowerCase();
-                const crate_alias_map = this.ALIASES.get(crate);
-                if (!crate_alias_map.has(word)) {
-                    crate_alias_map.set(word, []);
-                }
-                const aliases_map = crate_alias_map.get(word);
-
-                const normalizedName = word.indexOf("_") === -1 ? word : word.replace(/_/g, "");
-                for (const alias of alias_refs) {
-                    const originalIndex = alias + index;
-                    const original = searchIndex[originalIndex];
-                    /** @type {rustdoc.Row} */
-                    const row = {
-                        crate,
-                        name: alias_name,
-                        normalizedName,
-                        is_alias: true,
-                        ty: original.ty,
-                        type: original.type,
-                        paramNames: [],
-                        word,
-                        id,
-                        parent: undefined,
-                        original,
-                        path: "",
-                        implDisambiguator: original.implDisambiguator,
-                        // Needed to load the description of the original item.
-                        // @ts-ignore
-                        descShard: original.descShard,
-                        descIndex: original.descIndex,
-                        bitIndex: original.bitIndex,
-                    };
-                    aliases_map.push(row);
-                    this.nameTrie.insert(normalizedName, id, this.tailTable);
-                    id += 1;
-                    searchIndex.push(row);
-                }
-            }
-        }
-        // Drop the (rather large) hash table used for reusing function items
-        this.TYPES_POOL = new Map();
-        return searchIndex;
+        this.typeNameIdOfOutput = await first(output, TY_ASSOCTYPE, "");
+        this.typeNameIdOfFnPtr = await first(fn, TY_PRIMITIVE, "");
+        this.typeNameIdOfFn = await first(fn, TY_TRAIT, "core::ops");
+        this.typeNameIdOfFnMut = await first(fnMut, TY_TRAIT, "core::ops");
+        this.typeNameIdOfFnOnce = await first(fnOnce, TY_TRAIT, "core::ops");
+        this.typeNameIdOfArray = await first(array, TY_PRIMITIVE, "");
+        this.typeNameIdOfSlice = await first(slice, TY_PRIMITIVE, "");
+        this.typeNameIdOfArrayOrSlice = await first(arrayOrSlice, TY_PRIMITIVE, "");
+        this.typeNameIdOfTuple = await first(tuple, TY_PRIMITIVE, "");
+        this.typeNameIdOfUnit = await first(unit, TY_PRIMITIVE, "");
+        this.typeNameIdOfTupleOrUnit = await first(tupleOrUnit, TY_PRIMITIVE, "");
+        this.typeNameIdOfReference = await first(reference, TY_PRIMITIVE, "");
+        this.typeNameIdOfPointer = await first(pointer, TY_PRIMITIVE, "");
+        this.typeNameIdOfHof = await first(hof, TY_PRIMITIVE, "");
+        this.typeNameIdOfNever = await first(never, TY_PRIMITIVE, "");
     }
 
     /**
@@ -2349,41 +1304,6 @@ class DocSearch {
      */
     static parseQuery(userQuery) {
         /**
-         * @param {string} typename
-         * @returns {number}
-         */
-        function itemTypeFromName(typename) {
-            const index = itemTypes.findIndex(i => i === typename);
-            if (index < 0) {
-                throw ["Unknown type filter ", typename];
-            }
-            return index;
-        }
-
-        /**
-         * @param {rustdoc.ParserQueryElement} elem
-         */
-        function convertTypeFilterOnElem(elem) {
-            if (typeof elem.typeFilter === "string") {
-                let typeFilter = elem.typeFilter;
-                if (typeFilter === "const") {
-                    typeFilter = "constant";
-                }
-                elem.typeFilter = itemTypeFromName(typeFilter);
-            } else {
-                elem.typeFilter = NO_TYPE_FILTER;
-            }
-            for (const elem2 of elem.generics) {
-                convertTypeFilterOnElem(elem2);
-            }
-            for (const constraints of elem.bindings.values()) {
-                for (const constraint of constraints) {
-                    convertTypeFilterOnElem(constraint);
-                }
-            }
-        }
-
-        /**
          * Takes the user search input and returns an empty `ParsedQuery`.
          *
          * @param {string} userQuery
@@ -2443,8 +1363,7 @@ class DocSearch {
                     continue;
                 }
                 if (!foundStopChar) {
-                    /** @type String[] */
-                    let extra = [];
+                    let extra = EMPTY_STRING_ARRAY;
                     if (isLastElemGeneric(query.elems, parserState)) {
                         extra = [" after ", ">"];
                     } else if (prevIs(parserState, "\"")) {
@@ -2521,11 +1440,33 @@ class DocSearch {
 
         try {
             parseInput(query, parserState);
+
+            // Scan for invalid type filters, so that we can report the error
+            // outside the search loop.
+            /** @param {rustdoc.ParserQueryElement} elem */
+            const checkTypeFilter = elem => {
+                const ty = itemTypeFromName(elem.typeFilter);
+                if (ty === TY_GENERIC && elem.generics.length !== 0) {
+                    throw [
+                        "Generic type parameter ",
+                        elem.name,
+                        " does not accept generic parameters",
+                    ];
+                }
+                for (const generic of elem.generics) {
+                    checkTypeFilter(generic);
+                }
+                for (const constraints of elem.bindings.values()) {
+                    for (const constraint of constraints) {
+                        checkTypeFilter(constraint);
+                    }
+                }
+            };
             for (const elem of query.elems) {
-                convertTypeFilterOnElem(elem);
+                checkTypeFilter(elem);
             }
             for (const elem of query.returned) {
-                convertTypeFilterOnElem(elem);
+                checkTypeFilter(elem);
             }
         } catch (err) {
             query = newParsedQuery(userQuery);
@@ -2549,208 +1490,573 @@ class DocSearch {
     }
 
     /**
-     * Executes the parsed query and builds a {ResultsTable}.
-     *
-     * @param  {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} origParsedQuery
-     *     - The parsed user query
-     * @param  {Object} filterCrates - Crate to search in if defined
-     * @param  {string} currentCrate - Current crate, to rank results from this crate higher
-     *
-     * @return {Promise<rustdoc.ResultsTable>}
+     * @param {number} id
+     * @returns {Promise<string|null>}
      */
-    async execQuery(origParsedQuery, filterCrates, currentCrate) {
-        /** @type {rustdoc.Results} */
-        const results_others = new Map(),
-            /** @type {rustdoc.Results} */
-            results_in_args = new Map(),
-            /** @type {rustdoc.Results} */
-            results_returned = new Map();
-
-        /** @type {rustdoc.ParsedQuery<rustdoc.QueryElement>} */
-        // @ts-expect-error
-        const parsedQuery = origParsedQuery;
+    async getName(id) {
+        const ni = this.database.getData("name");
+        if (!ni) {
+            return null;
+        }
+        const name = await ni.at(id);
+        return name === undefined || name === null ? null : this.utf8decoder.decode(name);
+    }
 
-        const queryLen =
-            parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) +
-            parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0);
-        const maxEditDistance = Math.floor(queryLen / 3);
-        // We reinitialize the `FOUND_ALIASES` map.
-        this.FOUND_ALIASES.clear();
+    /**
+     * @param {number} id
+     * @returns {Promise<string|null>}
+     */
+    async getDesc(id) {
+        const di = this.database.getData("desc");
+        if (!di) {
+            return null;
+        }
+        const desc = await di.at(id);
+        return desc === undefined || desc === null ? null : this.utf8decoder.decode(desc);
+    }
 
+    /**
+     * @param {number} id
+     * @returns {Promise<number|null>}
+     */
+    async getAliasTarget(id) {
+        const ai = this.database.getData("alias");
+        if (!ai) {
+            return null;
+        }
+        const bytes = await ai.at(id);
+        if (bytes === undefined || bytes === null || bytes.length === 0) {
+            return null;
+        } else {
+            /** @type {string} */
+            const encoded = this.utf8decoder.decode(bytes);
+            /** @type {number|null} */
+            const decoded = JSON.parse(encoded);
+            return decoded;
+        }
+    }
+
+    /**
+     * @param {number} id
+     * @returns {Promise<rustdoc.EntryData|null>}
+     */
+    async getEntryData(id) {
+        const ei = this.database.getData("entry");
+        if (!ei) {
+            return null;
+        }
+        const encoded = this.utf8decoder.decode(await ei.at(id));
+        if (encoded === "" || encoded === undefined || encoded === null) {
+            return null;
+        }
+        /**
+         * krate,
+         * ty,
+         * module_path,
+         * exact_module_path,
+         * parent,
+         * deprecated,
+         * associated_item_disambiguator
+         * @type {rustdoc.ArrayWithOptionals<[
+         *     number,
+         *     rustdoc.ItemType,
+         *     number,
+         *     number,
+         *     number,
+         *     number,
+         * ], [string]>}
+         */
+        const raw = JSON.parse(encoded);
+        return {
+            krate: raw[0],
+            ty: raw[1],
+            modulePath: raw[2] === 0 ? null : raw[2] - 1,
+            exactModulePath: raw[3] === 0 ? null : raw[3] - 1,
+            parent: raw[4] === 0 ? null : raw[4] - 1,
+            deprecated: raw[5] === 1 ? true : false,
+            associatedItemDisambiguator: raw.length === 6 ? null : raw[6],
+        };
+    }
+
+    /**
+     * @param {number} id
+     * @returns {Promise<rustdoc.PathData|null>}
+     */
+    async getPathData(id) {
+        const pi = this.database.getData("path");
+        if (!pi) {
+            return null;
+        }
+        const encoded = this.utf8decoder.decode(await pi.at(id));
+        if (encoded === "" || encoded === undefined || encoded === null) {
+            return null;
+        }
         /**
-         * @type {Map<string, number>}
+         * ty, module_path, exact_module_path, search_unbox, inverted_function_signature_index
+         * @type {rustdoc.ArrayWithOptionals<[rustdoc.ItemType, string], [string|0, 0|1, string]>}
          */
-        const genericSymbols = new Map();
+        const raw = JSON.parse(encoded);
+        return {
+            ty: raw[0],
+            modulePath: raw[1],
+            exactModulePath: raw[2] === 0 || raw[2] === undefined ? raw[1] : raw[2],
+        };
+    }
 
+    /**
+     * @param {number} id
+     * @returns {Promise<rustdoc.FunctionData|null>}
+     */
+    async getFunctionData(id) {
+        const fi = this.database.getData("function");
+        if (!fi) {
+            return null;
+        }
+        const encoded = this.utf8decoder.decode(await fi.at(id));
+        if (encoded === "" || encoded === undefined || encoded === null) {
+            return null;
+        }
         /**
-         * Convert names to ids in parsed query elements.
-         * This is not used for the "In Names" tab, but is used for the
-         * "In Params", "In Returns", and "In Function Signature" tabs.
-         *
-         * If there is no matching item, but a close-enough match, this
-         * function also that correction.
-         *
-         * See `buildTypeMapIndex` for more information.
-         *
-         * @param {rustdoc.QueryElement} elem
-         * @param {boolean=} isAssocType
+         * function_signature, param_names
+         * @type {[string, string[]]}
          */
-        const convertNameToId = (elem, isAssocType) => {
-            const loweredName = elem.pathLast.toLowerCase();
-            if (this.typeNameIdMap.has(loweredName) &&
-                // @ts-expect-error
-                (isAssocType || !this.typeNameIdMap.get(loweredName).assocOnly)) {
-                // @ts-expect-error
-                elem.id = this.typeNameIdMap.get(loweredName).id;
-            } else if (!parsedQuery.literalSearch) {
-                let match = null;
-                let matchDist = maxEditDistance + 1;
-                let matchName = "";
-                for (const [name, { id, assocOnly }] of this.typeNameIdMap) {
-                    const dist = Math.min(
-                        editDistance(name, loweredName, maxEditDistance),
-                        editDistance(name, elem.normalizedPathLast, maxEditDistance),
-                    );
-                    if (dist <= matchDist && dist <= maxEditDistance &&
-                        (isAssocType || !assocOnly)) {
-                        if (dist === matchDist && matchName > name) {
-                            continue;
-                        }
-                        match = id;
-                        matchDist = dist;
-                        matchName = name;
-                    }
-                }
-                if (match !== null) {
-                    parsedQuery.correction = matchName;
-                }
-                elem.id = match;
+        const raw = JSON.parse(encoded);
+
+        const parser = new VlqHexDecoder(raw[0], async functionSearchType => {
+            if (typeof functionSearchType === "number") {
+                return null;
+            }
+            const INPUTS_DATA = 0;
+            const OUTPUT_DATA = 1;
+            /** @type {Promise<rustdoc.FunctionType[]>} */
+            let inputs_;
+            /** @type {Promise<rustdoc.FunctionType[]>} */
+            let output_;
+            if (typeof functionSearchType[INPUTS_DATA] === "number") {
+                inputs_ = Promise.all([
+                    this.buildItemSearchType(functionSearchType[INPUTS_DATA]),
+                ]);
+            } else {
+                // @ts-ignore
+                inputs_ = this.buildItemSearchTypeAll(functionSearchType[INPUTS_DATA]);
             }
-            if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1
-                && elem.generics.length === 0 && elem.bindings.size === 0)
-                || elem.typeFilter === TY_GENERIC) {
-                const id = genericSymbols.get(elem.normalizedPathLast);
-                if (id !== undefined) {
-                    elem.id = id;
+            if (functionSearchType.length > 1) {
+                if (typeof functionSearchType[OUTPUT_DATA] === "number") {
+                    output_ = Promise.all([
+                        this.buildItemSearchType(functionSearchType[OUTPUT_DATA]),
+                    ]);
                 } else {
-                    elem.id = -(genericSymbols.size + 1);
-                    genericSymbols.set(elem.normalizedPathLast, elem.id);
-                }
-                if (elem.typeFilter === -1 && elem.normalizedPathLast.length >= 3) {
-                    // Silly heuristic to catch if the user probably meant
-                    // to not write a generic parameter. We don't use it,
-                    // just bring it up.
-                    const maxPartDistance = Math.floor(elem.normalizedPathLast.length / 3);
-                    let matchDist = maxPartDistance + 1;
-                    let matchName = "";
-                    for (const name of this.typeNameIdMap.keys()) {
-                        const dist = editDistance(
-                            name,
-                            elem.normalizedPathLast,
-                            maxPartDistance,
-                        );
-                        if (dist <= matchDist && dist <= maxPartDistance) {
-                            if (dist === matchDist && matchName > name) {
-                                continue;
-                            }
-                            matchDist = dist;
-                            matchName = name;
-                        }
-                    }
-                    if (matchName !== "") {
-                        parsedQuery.proposeCorrectionFrom = elem.name;
-                        parsedQuery.proposeCorrectionTo = matchName;
-                    }
+                    // @ts-expect-error
+                    output_ = this.buildItemSearchTypeAll(functionSearchType[OUTPUT_DATA]);
                 }
-                elem.typeFilter = TY_GENERIC;
-            }
-            if (elem.generics.length > 0 && elem.typeFilter === TY_GENERIC) {
-                // Rust does not have HKT
-                parsedQuery.error = [
-                    "Generic type parameter ",
-                    elem.name,
-                    " does not accept generic parameters",
-                ];
-            }
-            for (const elem2 of elem.generics) {
-                convertNameToId(elem2);
+            } else {
+                output_ = Promise.resolve(EMPTY_GENERICS_ARRAY);
             }
-            elem.bindings = new Map(Array.from(elem.bindings.entries())
-                .map(entry => {
-                    const [name, constraints] = entry;
+            /** @type {Promise<rustdoc.FunctionType[]>[]} */
+            const where_clause_ = [];
+            const l = functionSearchType.length;
+            for (let i = 2; i < l; ++i) {
+                where_clause_.push(typeof functionSearchType[i] === "number"
                     // @ts-expect-error
-                    if (!this.typeNameIdMap.has(name)) {
-                        parsedQuery.error = [
-                            "Type parameter ",
-                            // @ts-expect-error
-                            name,
-                            " does not exist",
-                        ];
-                        return [0, []];
-                    }
-                    for (const elem2 of constraints) {
-                        convertNameToId(elem2, false);
-                    }
-
+                    ? Promise.all([this.buildItemSearchType(functionSearchType[i])])
                     // @ts-expect-error
-                    return [this.typeNameIdMap.get(name).id, constraints];
-                }),
-            );
+                    : this.buildItemSearchTypeAll(functionSearchType[i]),
+                );
+            }
+            const [inputs, output, where_clause] = await Promise.all([
+                inputs_,
+                output_,
+                Promise.all(where_clause_),
+            ]);
+            return {
+                inputs, output, where_clause,
+            };
+        });
+
+        return {
+            functionSignature: await parser.next(),
+            paramNames: raw[1],
+            elemCount: parser.elemCount,
         };
+    }
 
-        for (const elem of parsedQuery.elems) {
-            convertNameToId(elem, false);
-            this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
+    /**
+     * @param {number} id
+     * @returns {Promise<rustdoc.TypeData|null>}
+     */
+    async getTypeData(id) {
+        const ti = this.database.getData("type");
+        if (!ti) {
+            return null;
         }
-        for (const elem of parsedQuery.returned) {
-            convertNameToId(elem, false);
-            this.buildFunctionTypeFingerprint(elem, parsedQuery.typeFingerprint);
+        const encoded = this.utf8decoder.decode(await ti.at(id));
+        if (encoded === "" || encoded === undefined || encoded === null) {
+            return null;
+        }
+        /**
+         * function_signature, param_names
+         * @type {[string, number] | [number] | [string] | [] | null}
+         */
+        const raw = JSON.parse(encoded);
+
+        if (!raw || raw.length === 0) {
+            return null;
+        }
+
+        let searchUnbox = false;
+        const invertedFunctionSignatureIndex = [];
+
+        if (typeof raw[0] === "string") {
+            if (raw[1]) {
+                searchUnbox = true;
+            }
+            // the inverted function signature index is a list of bitmaps,
+            // by number of types that appear in the function
+            let i = 0;
+            const pb = makeUint8ArrayFromBase64(raw[0]);
+            const l = pb.length;
+            while (i < l) {
+                if (pb[i] === 0) {
+                    invertedFunctionSignatureIndex.push(RoaringBitmap.empty());
+                    i += 1;
+                } else {
+                    const bitmap = new RoaringBitmap(pb, i);
+                    i += bitmap.consumed_len_bytes;
+                    invertedFunctionSignatureIndex.push(bitmap);
+                }
+            }
+        } else if (raw[0]) {
+            searchUnbox = true;
+        }
+
+        return { searchUnbox, invertedFunctionSignatureIndex };
+    }
+
+    /**
+     * @returns {Promise<string[]>}
+     */
+    async getCrateNameList() {
+        const crateNames = this.database.getData("crateNames");
+        if (!crateNames) {
+            return [];
+        }
+        const l = crateNames.length;
+        const names = [];
+        for (let i = 0; i < l; ++i) {
+            names.push(crateNames.at(i).then(name => {
+                if (name === undefined) {
+                    return "";
+                }
+                return this.utf8decoder.decode(name);
+            }));
+        }
+        return Promise.all(names);
+    }
+
+    /**
+     * @param {number} id non-negative generic index
+     * @returns {Promise<stringdex.RoaringBitmap[]>}
+     */
+    async getGenericInvertedIndex(id) {
+        const gii = this.database.getData("generic_inverted_index");
+        if (!gii) {
+            return [];
+        }
+        const pb = await gii.at(id);
+        if (pb === undefined || pb === null || pb.length === 0) {
+            return [];
+        }
+
+        const invertedFunctionSignatureIndex = [];
+        // the inverted function signature index is a list of bitmaps,
+        // by number of types that appear in the function
+        let i = 0;
+        const l = pb.length;
+        while (i < l) {
+            if (pb[i] === 0) {
+                invertedFunctionSignatureIndex.push(RoaringBitmap.empty());
+                i += 1;
+            } else {
+                const bitmap = new RoaringBitmap(pb, i);
+                i += bitmap.consumed_len_bytes;
+                invertedFunctionSignatureIndex.push(bitmap);
+            }
+        }
+        return invertedFunctionSignatureIndex;
+    }
+
+    /**
+     * @param {number} id
+     * @returns {Promise<rustdoc.Row?>}
+     */
+    async getRow(id) {
+        const [name_, entry, path, type] = await Promise.all([
+            this.getName(id),
+            this.getEntryData(id),
+            this.getPathData(id),
+            this.getFunctionData(id),
+        ]);
+        if (!entry && !path) {
+            return null;
         }
+        const [
+            moduleName,
+            modulePathData,
+            exactModuleName,
+            exactModulePathData,
+        ] = await Promise.all([
+            entry && entry.modulePath !== null ? this.getName(entry.modulePath) : null,
+            entry && entry.modulePath !== null ? this.getPathData(entry.modulePath) : null,
+            entry && entry.exactModulePath !== null ?
+                this.getName(entry.exactModulePath) :
+                null,
+            entry && entry.exactModulePath !== null ?
+                this.getPathData(entry.exactModulePath) :
+                null,
+        ]);
+        const name = name_ === null ? "" : name_;
+        const normalizedName = (name.indexOf("_") === -1 ?
+            name :
+            name.replace(/_/g, "")).toLowerCase();
+        const modulePath = modulePathData === null || moduleName === null ? "" :
+            (modulePathData.modulePath === "" ?
+                moduleName :
+                `${modulePathData.modulePath}::${moduleName}`);
+        const [parentName, parentPath] = entry !== null && entry.parent !== null ?
+            await Promise.all([this.getName(entry.parent), this.getPathData(entry.parent)]) :
+            [null, null];
+        return {
+            id,
+            crate: entry ? nonnull(await this.getName(entry.krate)) : "",
+            ty: entry ? entry.ty : nonnull(path).ty,
+            name,
+            normalizedName,
+            modulePath,
+            exactModulePath: exactModulePathData === null || exactModuleName === null ? modulePath :
+                (exactModulePathData.exactModulePath === "" ?
+                    exactModuleName :
+                    `${exactModulePathData.exactModulePath}::${exactModuleName}`),
+            entry,
+            path,
+            type,
+            deprecated: entry ? entry.deprecated : false,
+            parent: parentName !== null && parentPath !== null ?
+                { name: parentName, path: parentPath } :
+                null,
+        };
+    }
 
+    /**
+     * Convert a list of RawFunctionType / ID to object-based FunctionType.
+     *
+     * Crates often have lots of functions in them, and it's common to have a large number of
+     * functions that operate on a small set of data types, so the search index compresses them
+     * by encoding function parameter and return types as indexes into an array of names.
+     *
+     * Even when a general-purpose compression algorithm is used, this is still a win.
+     * I checked. https://github.com/rust-lang/rust/pull/98475#issue-1284395985
+     *
+     * The format for individual function types is encoded in
+     * librustdoc/html/render/mod.rs: impl Serialize for RenderType
+     *
+     * @param {null|Array<rustdoc.RawFunctionType>} types
+     *
+     * @return {Promise<Array<rustdoc.FunctionType>>}
+     */
+    async buildItemSearchTypeAll(types) {
+        return types && types.length > 0 ?
+            await Promise.all(types.map(type => this.buildItemSearchType(type))) :
+            EMPTY_GENERICS_ARRAY;
+    }
 
+    /**
+     * Converts a single type.
+     *
+     * @param {rustdoc.RawFunctionType} type
+     * @return {Promise<rustdoc.FunctionType>}
+     */
+    async buildItemSearchType(type) {
+        const PATH_INDEX_DATA = 0;
+        const GENERICS_DATA = 1;
+        const BINDINGS_DATA = 2;
+        let id, generics;
         /**
-         * Creates the query results.
-         *
-         * @param {Array<rustdoc.ResultObject>} results_in_args
-         * @param {Array<rustdoc.ResultObject>} results_returned
-         * @param {Array<rustdoc.ResultObject>} results_others
-         * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} parsedQuery
-         *
-         * @return {rustdoc.ResultsTable}
+         * @type {Map<number, rustdoc.FunctionType[]>}
          */
-        function createQueryResults(
-            results_in_args,
-            results_returned,
-            results_others,
-            parsedQuery) {
-            return {
-                "in_args": results_in_args,
-                "returned": results_returned,
-                "others": results_others,
-                "query": parsedQuery,
+        let bindings;
+        if (typeof type === "number") {
+            id = type;
+            generics = EMPTY_GENERICS_ARRAY;
+            bindings = EMPTY_BINDINGS_MAP;
+        } else {
+            id = type[PATH_INDEX_DATA];
+            generics = await this.buildItemSearchTypeAll(type[GENERICS_DATA]);
+            if (type[BINDINGS_DATA] && type[BINDINGS_DATA].length > 0) {
+                bindings = new Map((await Promise.all(type[BINDINGS_DATA].map(
+                    /**
+                     * @param {[rustdoc.RawFunctionType, rustdoc.RawFunctionType[]]} binding
+                     * @returns {Promise<[number, rustdoc.FunctionType[]][]>}
+                    */
+                    async binding => {
+                        const [assocType, constraints] = binding;
+                        // Associated type constructors are represented sloppily in rustdoc's
+                        // type search, to make the engine simpler.
+                        //
+                        // MyType<Output<T>=Result<T>> is equivalent to MyType<Output<Result<T>>=T>
+                        // and both are, essentially
+                        // MyType<Output=(T, Result<T>)>, except the tuple isn't actually there.
+                        // It's more like the value of a type binding is naturally an array,
+                        // which rustdoc calls "constraints".
+                        //
+                        // As a result, the key should never have generics on it.
+                        const [k, v] = await Promise.all([
+                            this.buildItemSearchType(assocType).then(t => t.id),
+                            this.buildItemSearchTypeAll(constraints),
+                        ]);
+                        return k === null ? EMPTY_BINDINGS_ARRAY : [[k, v]];
+                    },
+                ))).flat());
+            } else {
+                bindings = EMPTY_BINDINGS_MAP;
+            }
+        }
+        /**
+         * @type {rustdoc.FunctionType}
+         */
+        let result;
+        if (id < 0) {
+            // types less than 0 are generic parameters
+            // the actual names of generic parameters aren't stored, since they aren't API
+            result = {
+                id,
+                name: "",
+                ty: TY_GENERIC,
+                path: null,
+                exactPath: null,
+                generics,
+                bindings,
+                unboxFlag: true,
             };
+        } else if (id === 0) {
+            // `0` is used as a sentinel because it's fewer bytes than `null`
+            result = {
+                id: null,
+                name: "",
+                ty: TY_GENERIC,
+                path: null,
+                exactPath: null,
+                generics,
+                bindings,
+                unboxFlag: true,
+            };
+        } else {
+            const [name, path, type] = await Promise.all([
+                this.getName(id - 1),
+                this.getPathData(id - 1),
+                this.getTypeData(id - 1),
+            ]);
+            if (path === undefined || path === null || type === undefined || type === null) {
+                return {
+                    id: null,
+                    name: "",
+                    ty: TY_GENERIC,
+                    path: null,
+                    exactPath: null,
+                    generics,
+                    bindings,
+                    unboxFlag: true,
+                };
+            }
+            result = {
+                id: id - 1,
+                name,
+                ty: path.ty,
+                path: path.modulePath,
+                exactPath: path.exactModulePath === null ? path.modulePath : path.exactModulePath,
+                generics,
+                bindings,
+                unboxFlag: type.searchUnbox,
+            };
+        }
+        const cr = this.TYPES_POOL.get(result.id);
+        if (cr) {
+            // Shallow equality check. Since this function is used
+            // to construct every type object, this should be mostly
+            // equivalent to a deep equality check, except if there's
+            // a conflict, we don't keep the old one around, so it's
+            // not a fully precise implementation of hashcons.
+            if (cr.generics.length === result.generics.length &&
+                cr.generics !== result.generics &&
+                cr.generics.every((x, i) => result.generics[i] === x)
+            ) {
+                result.generics = cr.generics;
+            }
+            if (cr.bindings.size === result.bindings.size && cr.bindings !== result.bindings) {
+                let ok = true;
+                for (const [k, v] of cr.bindings.entries()) {
+                    const v2 = result.bindings.get(k);
+                    if (!v2) {
+                        ok = false;
+                        break;
+                    }
+                    if (v !== v2 && v.length === v2.length && v.every((x, i) => v2[i] === x)) {
+                        result.bindings.set(k, v);
+                    } else if (v !== v2) {
+                        ok = false;
+                        break;
+                    }
+                }
+                if (ok) {
+                    result.bindings = cr.bindings;
+                }
+            }
+            if (cr.ty === result.ty && cr.path === result.path
+                && cr.bindings === result.bindings && cr.generics === result.generics
+                && cr.ty === result.ty && cr.name === result.name
+                && cr.unboxFlag === result.unboxFlag
+            ) {
+                return cr;
+            }
         }
+        this.TYPES_POOL.set(result.id, result);
+        return result;
+    }
 
-        // @ts-expect-error
+    /**
+     * Executes the parsed query and builds a {ResultsTable}.
+     *
+     * @param  {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} parsedQuery
+     *     - The parsed user query
+     * @param  {Object} filterCrates - Crate to search in if defined
+     * @param  {string} currentCrate - Current crate, to rank results from this crate higher
+     *
+     * @return {Promise<rustdoc.ResultsTable>}
+     */
+    async execQuery(parsedQuery, filterCrates, currentCrate) {
+        const queryLen =
+            parsedQuery.elems.reduce((acc, next) => acc + next.pathLast.length, 0) +
+            parsedQuery.returned.reduce((acc, next) => acc + next.pathLast.length, 0);
+        const maxEditDistance = Math.floor(queryLen / 3);
+
+        /**
+         * @param {rustdoc.Row} item
+         * @returns {[string, string, string]}
+         */
         const buildHrefAndPath = item => {
             let displayPath;
             let href;
-            if (item.is_alias) {
-                this.FOUND_ALIASES.add(item.word);
-                item = item.original;
-            }
             const type = itemTypes[item.ty];
             const name = item.name;
-            let path = item.path;
-            let exactPath = item.exactPath;
+            let path = item.modulePath;
+            let exactPath = item.exactModulePath;
 
             if (type === "mod") {
                 displayPath = path + "::";
                 href = this.rootPath + path.replace(/::/g, "/") + "/" +
                     name + "/index.html";
             } else if (type === "import") {
-                displayPath = item.path + "::";
-                href = this.rootPath + item.path.replace(/::/g, "/") +
+                displayPath = item.modulePath + "::";
+                href = this.rootPath + item.modulePath.replace(/::/g, "/") +
                     "/index.html#reexport." + name;
             } else if (type === "primitive" || type === "keyword") {
                 displayPath = "";
@@ -2760,13 +2066,13 @@ class DocSearch {
             } else if (type === "externcrate") {
                 displayPath = "";
                 href = this.rootPath + name + "/index.html";
-            } else if (item.parent !== undefined) {
+            } else if (item.parent) {
                 const myparent = item.parent;
                 let anchor = type + "." + name;
-                const parentType = itemTypes[myparent.ty];
+                const parentType = itemTypes[myparent.path.ty];
                 let pageType = parentType;
                 let pageName = myparent.name;
-                exactPath = `${myparent.exactPath}::${myparent.name}`;
+                exactPath = `${myparent.path.exactModulePath}::${myparent.name}`;
 
                 if (parentType === "primitive") {
                     displayPath = myparent.name + "::";
@@ -2774,9 +2080,9 @@ class DocSearch {
                 } else if (type === "structfield" && parentType === "variant") {
                     // Structfields belonging to variants are special: the
                     // final path element is the enum name.
-                    const enumNameIdx = item.path.lastIndexOf("::");
-                    const enumName = item.path.substr(enumNameIdx + 2);
-                    path = item.path.substr(0, enumNameIdx);
+                    const enumNameIdx = item.modulePath.lastIndexOf("::");
+                    const enumName = item.modulePath.substr(enumNameIdx + 2);
+                    path = item.modulePath.substr(0, enumNameIdx);
                     displayPath = path + "::" + enumName + "::" + myparent.name + "::";
                     anchor = "variant." + myparent.name + ".field." + name;
                     pageType = "enum";
@@ -2784,16 +2090,16 @@ class DocSearch {
                 } else {
                     displayPath = path + "::" + myparent.name + "::";
                 }
-                if (item.implDisambiguator !== null) {
-                    anchor = item.implDisambiguator + "/" + anchor;
+                if (item.entry && item.entry.associatedItemDisambiguator !== null) {
+                    anchor = item.entry.associatedItemDisambiguator + "/" + anchor;
                 }
                 href = this.rootPath + path.replace(/::/g, "/") +
                     "/" + pageType +
                     "." + pageName +
                     ".html#" + anchor;
             } else {
-                displayPath = item.path + "::";
-                href = this.rootPath + item.path.replace(/::/g, "/") +
+                displayPath = item.modulePath + "::";
+                href = this.rootPath + item.modulePath.replace(/::/g, "/") +
                     "/" + type + "." + name + ".html";
             }
             return [displayPath, href, `${exactPath}::${name}`];
@@ -2816,82 +2122,16 @@ class DocSearch {
          * Add extra data to result objects, and filter items that have been
          * marked for removal.
          *
-         * @param {rustdoc.ResultObject[]} results
-         * @param {"sig"|"elems"|"returned"|null} typeInfo
-         * @returns {rustdoc.ResultObject[]}
-         */
-        const transformResults = (results, typeInfo) => {
-            const duplicates = new Set();
-            const out = [];
-
-            for (const result of results) {
-                if (result.id !== -1) {
-                    const res = buildHrefAndPath(this.searchIndex[result.id]);
-                    // many of these properties don't strictly need to be
-                    // copied over, but copying them over satisfies tsc,
-                    // and hopefully plays nice with the shape optimization
-                    // of the browser engine.
-                    /** @type {rustdoc.ResultObject} */
-                    const obj = Object.assign({
-                        parent: result.parent,
-                        type: result.type,
-                        dist: result.dist,
-                        path_dist: result.path_dist,
-                        index: result.index,
-                        desc: result.desc,
-                        item: result.item,
-                        displayPath: pathSplitter(res[0]),
-                        fullPath: "",
-                        href: "",
-                        displayTypeSignature: null,
-                    }, this.searchIndex[result.id]);
-
-                    // To be sure than it some items aren't considered as duplicate.
-                    obj.fullPath = res[2] + "|" + obj.ty;
-
-                    if (duplicates.has(obj.fullPath)) {
-                        continue;
-                    }
-
-                    // Exports are specifically not shown if the items they point at
-                    // are already in the results.
-                    if (obj.ty === TY_IMPORT && duplicates.has(res[2])) {
-                        continue;
-                    }
-                    if (duplicates.has(res[2] + "|" + TY_IMPORT)) {
-                        continue;
-                    }
-                    duplicates.add(obj.fullPath);
-                    duplicates.add(res[2]);
-
-                    if (typeInfo !== null) {
-                        obj.displayTypeSignature =
-                            // @ts-expect-error
-                            this.formatDisplayTypeSignature(obj, typeInfo);
-                    }
-
-                    obj.href = res[1];
-                    out.push(obj);
-                    if (out.length >= MAX_RESULTS) {
-                        break;
-                    }
-                }
-            }
-            return out;
-        };
-
-        /**
-         * Add extra data to result objects, and filter items that have been
-         * marked for removal.
-         *
          * The output is formatted as an array of hunks, where odd numbered
          * hunks are highlighted and even numbered ones are not.
          *
          * @param {rustdoc.ResultObject} obj
          * @param {"sig"|"elems"|"returned"|null} typeInfo
+         * @param {rustdoc.QueryElement[]} elems
+         * @param {rustdoc.QueryElement[]} returned
          * @returns {Promise<rustdoc.DisplayTypeSignature>}
          */
-        this.formatDisplayTypeSignature = async(obj, typeInfo) => {
+        const formatDisplayTypeSignature = async(obj, typeInfo, elems, returned) => {
             const objType = obj.type;
             if (!objType) {
                 return {type: [], mappedNames: new Map(), whereClause: new Map()};
@@ -2903,13 +2143,13 @@ class DocSearch {
             if (typeInfo !== "elems" && typeInfo !== "returned") {
                 fnInputs = unifyFunctionTypes(
                     objType.inputs,
-                    parsedQuery.elems,
+                    elems,
                     objType.where_clause,
                     null,
                     mgensScratch => {
                         fnOutput = unifyFunctionTypes(
                             objType.output,
-                            parsedQuery.returned,
+                            returned,
                             objType.where_clause,
                             mgensScratch,
                             mgensOut => {
@@ -2923,10 +2163,9 @@ class DocSearch {
                     0,
                 );
             } else {
-                const arr = typeInfo === "elems" ? objType.inputs : objType.output;
                 const highlighted = unifyFunctionTypes(
-                    arr,
-                    parsedQuery.elems,
+                    typeInfo === "elems" ? objType.inputs : objType.output,
+                    typeInfo === "elems" ? elems : returned,
                     objType.where_clause,
                     null,
                     mgensOut => {
@@ -2975,15 +2214,15 @@ class DocSearch {
                 }
             };
 
-            parsedQuery.elems.forEach(remapQuery);
-            parsedQuery.returned.forEach(remapQuery);
+            elems.forEach(remapQuery);
+            returned.forEach(remapQuery);
 
             /**
              * Write text to a highlighting array.
              * Index 0 is not highlighted, index 1 is highlighted,
              * index 2 is not highlighted, etc.
              *
-             * @param {{name?: string, highlighted?: boolean}} fnType - input
+             * @param {{name: string|null, highlighted?: boolean}} fnType - input
              * @param {string[]} result
              */
             const pushText = (fnType, result) => {
@@ -3010,8 +2249,9 @@ class DocSearch {
              *
              * @param {rustdoc.HighlightedFunctionType} fnType - input
              * @param {string[]} result
+             * @returns {Promise<void>}
              */
-            const writeHof = (fnType, result) => {
+            const writeHof = async(fnType, result) => {
                 const hofOutput = fnType.bindings.get(this.typeNameIdOfOutput) || [];
                 const hofInputs = fnType.generics;
                 pushText(fnType, result);
@@ -3022,7 +2262,7 @@ class DocSearch {
                         pushText({ name: ", ", highlighted: false }, result);
                     }
                     needsComma = true;
-                    writeFn(fnType, result);
+                    await writeFn(fnType, result);
                 }
                 pushText({
                     name: hofOutput.length === 0 ? ")" : ") -> ",
@@ -3037,7 +2277,7 @@ class DocSearch {
                         pushText({ name: ", ", highlighted: false }, result);
                     }
                     needsComma = true;
-                    writeFn(fnType, result);
+                    await writeFn(fnType, result);
                 }
                 if (hofOutput.length > 1) {
                     pushText({name: ")", highlighted: false}, result);
@@ -3050,8 +2290,9 @@ class DocSearch {
              *
              * @param {rustdoc.HighlightedFunctionType} fnType
              * @param {string[]} result
+             * @returns {Promise<boolean>}
              */
-            const writeSpecialPrimitive = (fnType, result) => {
+            const writeSpecialPrimitive = async(fnType, result) => {
                 if (fnType.id === this.typeNameIdOfArray || fnType.id === this.typeNameIdOfSlice ||
                     fnType.id === this.typeNameIdOfTuple || fnType.id === this.typeNameIdOfUnit) {
                     const [ob, sb] =
@@ -3060,7 +2301,7 @@ class DocSearch {
                         ["[", "]"] :
                         ["(", ")"];
                     pushText({ name: ob, highlighted: fnType.highlighted }, result);
-                    onEachBtwn(
+                    await onEachBtwnAsync(
                         fnType.generics,
                         nested => writeFn(nested, result),
                         // @ts-expect-error
@@ -3071,11 +2312,11 @@ class DocSearch {
                 } else if (fnType.id === this.typeNameIdOfReference) {
                     pushText({ name: "&", highlighted: fnType.highlighted }, result);
                     let prevHighlighted = false;
-                    onEachBtwn(
+                    await onEachBtwnAsync(
                         fnType.generics,
-                        value => {
+                        async value => {
                             prevHighlighted = !!value.highlighted;
-                            writeFn(value, result);
+                            await writeFn(value, result);
                         },
                         // @ts-expect-error
                         value => pushText({
@@ -3084,8 +2325,35 @@ class DocSearch {
                         }, result),
                     );
                     return true;
-                } else if (fnType.id === this.typeNameIdOfFn) {
-                    writeHof(fnType, result);
+                } else if (fnType.id === this.typeNameIdOfPointer) {
+                    pushText({ name: "*", highlighted: fnType.highlighted }, result);
+                    if (fnType.generics.length < 2) {
+                        pushText({ name: "const ", highlighted: fnType.highlighted }, result);
+                    }
+                    let prevHighlighted = false;
+                    await onEachBtwnAsync(
+                        fnType.generics,
+                        async value => {
+                            prevHighlighted = !!value.highlighted;
+                            await writeFn(value, result);
+                        },
+                        // @ts-expect-error
+                        value => pushText({
+                            name: " ",
+                            highlighted: prevHighlighted && value.highlighted,
+                        }, result),
+                    );
+                    return true;
+                } else if (
+                    fnType.id === this.typeNameIdOfFn ||
+                    fnType.id === this.typeNameIdOfFnMut ||
+                    fnType.id === this.typeNameIdOfFnOnce ||
+                    fnType.id === this.typeNameIdOfFnPtr
+                ) {
+                    await writeHof(fnType, result);
+                    return true;
+                } else if (fnType.id === this.typeNameIdOfNever) {
+                    pushText({ name: "!", highlighted: fnType.highlighted }, result);
                     return true;
                 }
                 return false;
@@ -3097,8 +2365,9 @@ class DocSearch {
              *
              * @param {rustdoc.HighlightedFunctionType} fnType
              * @param {string[]} result
+             * @returns {Promise<void>}
              */
-            const writeFn = (fnType, result) => {
+            const writeFn = async(fnType, result) => {
                 if (fnType.id !== null && fnType.id < 0) {
                     if (fnParamNames[-1 - fnType.id] === "") {
                         // Normally, there's no need to shown an unhighlighted
@@ -3107,7 +2376,7 @@ class DocSearch {
                             fnType.generics :
                             objType.where_clause[-1 - fnType.id];
                         for (const nested of generics) {
-                            writeFn(nested, result);
+                            await writeFn(nested, result);
                         }
                         return;
                     } else if (mgens) {
@@ -3126,7 +2395,7 @@ class DocSearch {
                     }, result);
                     /** @type{string[]} */
                     const where = [];
-                    onEachBtwn(
+                    await onEachBtwnAsync(
                         fnType.generics,
                         nested => writeFn(nested, where),
                         // @ts-expect-error
@@ -3137,32 +2406,61 @@ class DocSearch {
                     }
                 } else {
                     if (fnType.ty === TY_PRIMITIVE) {
-                        if (writeSpecialPrimitive(fnType, result)) {
+                        if (await writeSpecialPrimitive(fnType, result)) {
                             return;
                         }
                     } else if (fnType.ty === TY_TRAIT && (
                         fnType.id === this.typeNameIdOfFn ||
-                            fnType.id === this.typeNameIdOfFnMut ||
-                            fnType.id === this.typeNameIdOfFnOnce)) {
-                        writeHof(fnType, result);
+                        fnType.id === this.typeNameIdOfFnMut ||
+                        fnType.id === this.typeNameIdOfFnOnce ||
+                        fnType.id === this.typeNameIdOfFnPtr
+                    )) {
+                        await writeHof(fnType, result);
+                        return;
+                    } else if (fnType.name === "" &&
+                        fnType.bindings.size === 0 &&
+                        fnType.generics.length !== 0
+                    ) {
+                        pushText({ name: "impl ", highlighted: false }, result);
+                        if (fnType.generics.length > 1) {
+                            pushText({ name: "(", highlighted: false }, result);
+                        }
+                        await onEachBtwnAsync(
+                            fnType.generics,
+                            value => writeFn(value, result),
+                            // @ts-expect-error
+                            () => pushText({ name: ", ",  highlighted: false }, result),
+                        );
+                        if (fnType.generics.length > 1) {
+                            pushText({ name: ")", highlighted: false }, result);
+                        }
                         return;
                     }
                     pushText(fnType, result);
                     let hasBindings = false;
                     if (fnType.bindings.size > 0) {
-                        onEachBtwn(
-                            fnType.bindings,
-                            ([key, values]) => {
-                                const name = this.assocTypeIdNameMap.get(key);
+                        await onEachBtwnAsync(
+                            await Promise.all([...fnType.bindings.entries()].map(
+                                /**
+                                 * @param {[number, rustdoc.HighlightedFunctionType[]]} param0
+                                 * @returns {Promise<[
+                                 *     string|null,
+                                 *     rustdoc.HighlightedFunctionType[],
+                                 * ]>}
+                                 */
+                                async([key, values]) => [await this.getName(key), values],
+                            )),
+                            async([name, values]) => {
                                 // @ts-expect-error
                                 if (values.length === 1 && values[0].id < 0 &&
                                     // @ts-expect-error
-                                    `${fnType.name}::${name}` === fnParamNames[-1 - values[0].id]) {
+                                    `${fnType.name}::${name}` === fnParamNames[-1 - values[0].id]
+                                ) {
                                     // the internal `Item=Iterator::Item` type variable should be
                                     // shown in the where clause and name mapping output, but is
                                     // redundant in this spot
                                     for (const value of values) {
-                                        writeFn(value, []);
+                                        await writeFn(value, []);
                                     }
                                     return true;
                                 }
@@ -3175,7 +2473,7 @@ class DocSearch {
                                     name: values.length !== 1 ? "=(" : "=",
                                     highlighted: false,
                                 }, result);
-                                onEachBtwn(
+                                await onEachBtwnAsync(
                                     values || [],
                                     value => writeFn(value, result),
                                     // @ts-expect-error
@@ -3192,7 +2490,7 @@ class DocSearch {
                     if (fnType.generics.length > 0) {
                         pushText({ name: hasBindings ? ", " : "<", highlighted: false }, result);
                     }
-                    onEachBtwn(
+                    await onEachBtwnAsync(
                         fnType.generics,
                         value => writeFn(value, result),
                         // @ts-expect-error
@@ -3205,14 +2503,14 @@ class DocSearch {
             };
             /** @type {string[]} */
             const type = [];
-            onEachBtwn(
+            await onEachBtwnAsync(
                 fnInputs,
                 fnType => writeFn(fnType, type),
                 // @ts-expect-error
                 () => pushText({ name: ", ",  highlighted: false }, type),
             );
             pushText({ name: " -> ", highlighted: false }, type);
-            onEachBtwn(
+            await onEachBtwnAsync(
                 fnOutput,
                 fnType => writeFn(fnType, type),
                 // @ts-expect-error
@@ -3223,195 +2521,252 @@ class DocSearch {
         };
 
         /**
-         * This function takes a result map, and sorts it by various criteria, including edit
-         * distance, substring match, and the crate it comes from.
+         * Add extra data to result objects, and filter items that have been
+         * marked for removal.
          *
-         * @param {rustdoc.Results} results
+         * @param {[rustdoc.PlainResultObject, rustdoc.Row][]} results
          * @param {"sig"|"elems"|"returned"|null} typeInfo
-         * @param {string} preferredCrate
-         * @returns {Promise<rustdoc.ResultObject[]>}
+         * @param {Set<string>} duplicates
+         * @returns {rustdoc.ResultObject[]}
          */
-        const sortResults = async(results, typeInfo, preferredCrate) => {
-            const userQuery = parsedQuery.userQuery;
-            const normalizedUserQuery = parsedQuery.userQuery.toLowerCase();
-            const isMixedCase = normalizedUserQuery !== userQuery;
-            const result_list = [];
-            const isReturnTypeQuery = parsedQuery.elems.length === 0 ||
-                typeInfo === "returned";
-            for (const result of results.values()) {
-                result.item = this.searchIndex[result.id];
-                result.word = this.searchIndex[result.id].word;
-                if (isReturnTypeQuery) {
-                    // We are doing a return-type based search, deprioritize "clone-like" results,
-                    // ie. functions that also take the queried type as an argument.
-                    const resultItemType = result.item && result.item.type;
-                    if (!resultItemType) {
+        const transformResults = (results, typeInfo, duplicates) => {
+            const out = [];
+
+            for (const [result, item] of results) {
+                if (item.id !== -1) {
+                    const res = buildHrefAndPath(item);
+                    // many of these properties don't strictly need to be
+                    // copied over, but copying them over satisfies tsc,
+                    // and hopefully plays nice with the shape optimization
+                    // of the browser engine.
+                    /** @type {rustdoc.ResultObject} */
+                    const obj = Object.assign({
+                        parent: item.parent ? {
+                            path: item.parent.path.modulePath,
+                            exactPath: item.parent.path.exactModulePath ||
+                                item.parent.path.modulePath,
+                            name: item.parent.name,
+                            ty: item.parent.path.ty,
+                        } : undefined,
+                        type: item.type && item.type.functionSignature ?
+                            item.type.functionSignature :
+                            undefined,
+                        paramNames: item.type && item.type.paramNames ?
+                            item.type.paramNames :
+                            undefined,
+                        dist: result.dist,
+                        path_dist: result.path_dist,
+                        index: result.index,
+                        desc: this.getDesc(result.id),
+                        item,
+                        displayPath: pathSplitter(res[0]),
+                        fullPath: "",
+                        href: "",
+                        displayTypeSignature: null,
+                    }, result);
+
+                    // To be sure than it some items aren't considered as duplicate.
+                    obj.fullPath = res[2] + "|" + obj.item.ty;
+
+                    if (duplicates.has(obj.fullPath)) {
                         continue;
                     }
-                    const inputs = resultItemType.inputs;
-                    const where_clause = resultItemType.where_clause;
-                    if (containsTypeFromQuery(inputs, where_clause)) {
-                        result.path_dist *= 100;
-                        result.dist *= 100;
+
+                    // Exports are specifically not shown if the items they point at
+                    // are already in the results.
+                    if (obj.item.ty === TY_IMPORT && duplicates.has(res[2])) {
+                        continue;
+                    }
+                    if (duplicates.has(res[2] + "|" + TY_IMPORT)) {
+                        continue;
+                    }
+                    duplicates.add(obj.fullPath);
+                    duplicates.add(res[2]);
+
+                    if (typeInfo !== null) {
+                        obj.displayTypeSignature = formatDisplayTypeSignature(
+                            obj,
+                            typeInfo,
+                            result.elems,
+                            result.returned,
+                        );
+                    }
+
+                    obj.href = res[1];
+                    out.push(obj);
+                    if (out.length >= MAX_RESULTS) {
+                        break;
                     }
                 }
-                result_list.push(result);
             }
 
-            result_list.sort((aaa, bbb) => {
-                /** @type {number} */
-                let a;
-                /** @type {number} */
-                let b;
+            return out;
+        };
 
-                // sort by exact case-sensitive match
-                if (isMixedCase) {
-                    a = Number(aaa.item.name !== userQuery);
-                    b = Number(bbb.item.name !== userQuery);
-                    if (a !== b) {
-                        return a - b;
+        const sortAndTransformResults =
+            /**
+             * @this {DocSearch}
+             * @param {Array<rustdoc.PlainResultObject|null>} results
+             * @param {"sig"|"elems"|"returned"|null} typeInfo
+             * @param {string} preferredCrate
+             * @param {Set<string>} duplicates
+             * @returns {AsyncGenerator<rustdoc.ResultObject, number>}
+             */
+            async function*(results, typeInfo, preferredCrate, duplicates) {
+                const userQuery = parsedQuery.userQuery;
+                const normalizedUserQuery = parsedQuery.userQuery.toLowerCase();
+                const isMixedCase = normalizedUserQuery !== userQuery;
+                /**
+                 * @type {[rustdoc.PlainResultObject, rustdoc.Row][]}
+                 */
+                const result_list = [];
+                for (const result of results.values()) {
+                    if (!result) {
+                        continue;
+                    }
+                    /**
+                     * @type {rustdoc.Row?}
+                     */
+                    const item = await this.getRow(result.id);
+                    if (!item) {
+                        continue;
+                    }
+                    if (filterCrates !== null && item.crate !== filterCrates) {
+                        continue;
+                    }
+                    if (item) {
+                        result_list.push([result, item]);
+                    } else {
+                        continue;
                     }
                 }
 
-                // sort by exact match with regard to the last word (mismatch goes later)
-                a = Number(aaa.word !== normalizedUserQuery);
-                b = Number(bbb.word !== normalizedUserQuery);
-                if (a !== b) {
-                    return a - b;
-                }
+                result_list.sort(([aaa, aai], [bbb, bbi]) => {
+                    /** @type {number} */
+                    let a;
+                    /** @type {number} */
+                    let b;
+
+                    if (typeInfo === null) {
+                        // in name based search...
+
+                        // sort by exact case-sensitive match
+                        if (isMixedCase) {
+                            a = Number(aai.name !== userQuery);
+                            b = Number(bbi.name !== userQuery);
+                            if (a !== b) {
+                                return a - b;
+                            }
+                        }
 
-                // sort by index of keyword in item name (no literal occurrence goes later)
-                a = Number(aaa.index < 0);
-                b = Number(bbb.index < 0);
-                if (a !== b) {
-                    return a - b;
-                }
+                        // sort by exact match with regard to the last word (mismatch goes later)
+                        a = Number(aai.normalizedName !== normalizedUserQuery);
+                        b = Number(bbi.normalizedName !== normalizedUserQuery);
+                        if (a !== b) {
+                            return a - b;
+                        }
 
-                // in type based search, put functions first
-                if (parsedQuery.hasReturnArrow) {
-                    a = Number(!isFnLikeTy(aaa.item.ty));
-                    b = Number(!isFnLikeTy(bbb.item.ty));
+                        // sort by index of keyword in item name (no literal occurrence goes later)
+                        a = Number(aaa.index < 0);
+                        b = Number(bbb.index < 0);
+                        if (a !== b) {
+                            return a - b;
+                        }
+                    }
+
+                    // Sort by distance in the path part, if specified
+                    // (less changes required to match means higher rankings)
+                    a = Number(aaa.path_dist);
+                    b = Number(bbb.path_dist);
                     if (a !== b) {
                         return a - b;
                     }
-                }
-
-                // Sort by distance in the path part, if specified
-                // (less changes required to match means higher rankings)
-                a = Number(aaa.path_dist);
-                b = Number(bbb.path_dist);
-                if (a !== b) {
-                    return a - b;
-                }
-
-                // (later literal occurrence, if any, goes later)
-                a = Number(aaa.index);
-                b = Number(bbb.index);
-                if (a !== b) {
-                    return a - b;
-                }
 
-                // Sort by distance in the name part, the last part of the path
-                // (less changes required to match means higher rankings)
-                a = Number(aaa.dist);
-                b = Number(bbb.dist);
-                if (a !== b) {
-                    return a - b;
-                }
+                    // (later literal occurrence, if any, goes later)
+                    a = Number(aaa.index);
+                    b = Number(bbb.index);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort deprecated items later
-                a = Number(
-                    // @ts-expect-error
-                    this.searchIndexDeprecated.get(aaa.item.crate).contains(aaa.item.bitIndex),
-                );
-                b = Number(
-                    // @ts-expect-error
-                    this.searchIndexDeprecated.get(bbb.item.crate).contains(bbb.item.bitIndex),
-                );
-                if (a !== b) {
-                    return a - b;
-                }
+                    // Sort by distance in the name part, the last part of the path
+                    // (less changes required to match means higher rankings)
+                    a = Number(aaa.dist);
+                    b = Number(bbb.dist);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort unstable items later
-                // FIXME: there is some doubt if this is the most effecient way to implement this.
-                // alternative options include:
-                // * put is_unstable on each item when the index is built.
-                //   increases memory usage but avoids a hashmap lookup.
-                // * put is_unstable on each item before sorting.
-                //   better worst case performance but worse average case performance.
-                a = Number(
-                    // @ts-expect-error
-                    this.searchIndexUnstable.get(aaa.item.crate).contains(aaa.item.bitIndex),
-                );
-                b = Number(
-                    // @ts-expect-error
-                    this.searchIndexUnstable.get(bbb.item.crate).contains(bbb.item.bitIndex),
-                );
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort aliases lower
+                    a = Number(aaa.is_alias);
+                    b = Number(bbb.is_alias);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort by crate (current crate comes first)
-                a = Number(aaa.item.crate !== preferredCrate);
-                b = Number(bbb.item.crate !== preferredCrate);
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort deprecated items later
+                    a = Number(aai.deprecated);
+                    b = Number(bbi.deprecated);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort by item name length (longer goes later)
-                a = Number(aaa.word.length);
-                b = Number(bbb.word.length);
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort by crate (current crate comes first)
+                    a = Number(aai.crate !== preferredCrate);
+                    b = Number(bbi.crate !== preferredCrate);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort doc alias items later
-                a = Number(aaa.item.is_alias === true);
-                b = Number(bbb.item.is_alias === true);
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort by item name length (longer goes later)
+                    a = Number(aai.normalizedName.length);
+                    b = Number(bbi.normalizedName.length);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort by item name (lexicographically larger goes later)
-                let aw = aaa.word;
-                let bw = bbb.word;
-                if (aw !== bw) {
-                    return (aw > bw ? +1 : -1);
-                }
+                    // sort by item name (lexicographically larger goes later)
+                    let aw = aai.normalizedName;
+                    let bw = bbi.normalizedName;
+                    if (aw !== bw) {
+                        return (aw > bw ? +1 : -1);
+                    }
 
-                // sort by description (no description goes later)
-                a = Number(
-                    // @ts-expect-error
-                    this.searchIndexEmptyDesc.get(aaa.item.crate).contains(aaa.item.bitIndex),
-                );
-                b = Number(
-                    // @ts-expect-error
-                    this.searchIndexEmptyDesc.get(bbb.item.crate).contains(bbb.item.bitIndex),
-                );
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort by description (no description goes later)
+                    const di = this.database.getData("desc");
+                    if (di) {
+                        a = Number(di.isEmpty(aaa.id));
+                        b = Number(di.isEmpty(bbb.id));
+                        if (a !== b) {
+                            return a - b;
+                        }
+                    }
 
-                // sort by type (later occurrence in `itemTypes` goes later)
-                a = Number(aaa.item.ty);
-                b = Number(bbb.item.ty);
-                if (a !== b) {
-                    return a - b;
-                }
+                    // sort by type (later occurrence in `itemTypes` goes later)
+                    a = Number(aai.ty);
+                    b = Number(bbi.ty);
+                    if (a !== b) {
+                        return a - b;
+                    }
 
-                // sort by path (lexicographically larger goes later)
-                aw = aaa.item.path;
-                bw = bbb.item.path;
-                if (aw !== bw) {
-                    return (aw > bw ? +1 : -1);
-                }
+                    // sort by path (lexicographically larger goes later)
+                    const ap = aai.modulePath;
+                    const bp = bbi.modulePath;
+                    aw = ap === undefined ? "" : ap;
+                    bw = bp === undefined ? "" : bp;
+                    if (aw !== bw) {
+                        return (aw > bw ? +1 : -1);
+                    }
 
-                // que sera, sera
-                return 0;
-            });
+                    // que sera, sera
+                    return 0;
+                });
 
-            return transformResults(result_list, typeInfo);
-        };
+                const transformed_result_list = transformResults(result_list, typeInfo, duplicates);
+                yield* transformed_result_list;
+                return transformed_result_list.length;
+            }
+            .bind(this);
 
         /**
          * This function checks if a list of search query `queryElems` can all be found in the
@@ -3963,6 +3318,8 @@ class DocSearch {
                 }
                 return true;
             } else {
+                // For these special cases, matching code need added to the inverted index.
+                // search_index.rs -> convert_render_type does this
                 if (queryElem.id === this.typeNameIdOfArrayOrSlice &&
                     (fnType.id === this.typeNameIdOfSlice || fnType.id === this.typeNameIdOfArray)
                 ) {
@@ -3973,10 +3330,12 @@ class DocSearch {
                 ) {
                     // () matches primitive:tuple or primitive:unit
                     // if it matches, then we're fine, and this is an appropriate match candidate
-                } else if (queryElem.id === this.typeNameIdOfHof &&
-                    (fnType.id === this.typeNameIdOfFn || fnType.id === this.typeNameIdOfFnMut ||
-                        fnType.id === this.typeNameIdOfFnOnce)
-                ) {
+                } else if (queryElem.id === this.typeNameIdOfHof && (
+                    fnType.id === this.typeNameIdOfFn ||
+                    fnType.id === this.typeNameIdOfFnMut ||
+                    fnType.id === this.typeNameIdOfFnOnce ||
+                    fnType.id === this.typeNameIdOfFnPtr
+                )) {
                     // -> matches fn, fnonce, and fnmut
                     // if it matches, then we're fine, and this is an appropriate match candidate
                 } else if (fnType.id !== queryElem.id || queryElem.id === null) {
@@ -4159,21 +3518,13 @@ class DocSearch {
          * This function checks if the given list contains any
          * (non-generic) types mentioned in the query.
          *
+         * @param {rustdoc.QueryElement[]} elems
          * @param {rustdoc.FunctionType[]} list    - A list of function types.
          * @param {rustdoc.FunctionType[][]} where_clause - Trait bounds for generic items.
          */
-        function containsTypeFromQuery(list, where_clause) {
+        function containsTypeFromQuery(elems, list, where_clause) {
             if (!list) return false;
-            for (const ty of parsedQuery.returned) {
-                // negative type ids are generics
-                if (ty.id !== null && ty.id < 0) {
-                    continue;
-                }
-                if (checkIfInList(list, ty, where_clause, null, 0)) {
-                    return true;
-                }
-            }
-            for (const ty of parsedQuery.elems) {
+            for (const ty of elems) {
                 if (ty.id !== null && ty.id < 0) {
                     continue;
                 }
@@ -4265,10 +3616,10 @@ class DocSearch {
         /**
          * Compute an "edit distance" that ignores missing path elements.
          * @param {string[]} contains search query path
-         * @param {rustdoc.Row} ty indexed item
+         * @param {string[]} path indexed page path
          * @returns {null|number} edit distance
          */
-        function checkPath(contains, ty) {
+        function checkPath(contains, path) {
             if (contains.length === 0) {
                 return 0;
             }
@@ -4276,11 +3627,6 @@ class DocSearch {
                 contains.reduce((acc, next) => acc + next.length, 0) / 3,
             );
             let ret_dist = maxPathEditDistance + 1;
-            const path = ty.path.split("::");
-
-            if (ty.parent && ty.parent.name) {
-                path.push(ty.parent.name.toLowerCase());
-            }
 
             const length = path.length;
             const clength = contains.length;
@@ -4306,7 +3652,32 @@ class DocSearch {
             return ret_dist > maxPathEditDistance ? null : ret_dist;
         }
 
-        // @ts-expect-error
+        /**
+         * Compute an "edit distance" that ignores missing path elements.
+         * @param {string[]} contains search query path
+         * @param {rustdoc.Row} row indexed item
+         * @returns {null|number} edit distance
+         */
+        function checkRowPath(contains, row) {
+            if (contains.length === 0) {
+                return 0;
+            }
+
+            const path = row.modulePath.split("::");
+
+            if (row.parent && row.parent.name) {
+                path.push(row.parent.name.toLowerCase());
+            }
+
+            return checkPath(contains, path);
+        }
+
+        /**
+         *
+         * @param {number} filter
+         * @param {rustdoc.ItemType} type
+         * @returns
+         */
         function typePassesFilter(filter, type) {
             // No filter or Exact mach
             if (filter <= NO_TYPE_FILTER || filter === type) return true;
@@ -4328,366 +3699,839 @@ class DocSearch {
             return false;
         }
 
-        // @ts-expect-error
-        const handleAliases = async(ret, query, filterCrates, currentCrate) => {
-            const lowerQuery = query.toLowerCase();
-            if (this.FOUND_ALIASES.has(lowerQuery)) {
-                return;
-            }
-            this.FOUND_ALIASES.add(lowerQuery);
-            // We separate aliases and crate aliases because we want to have current crate
-            // aliases to be before the others in the displayed results.
-            // @ts-expect-error
-            const aliases = [];
-            // @ts-expect-error
-            const crateAliases = [];
-            if (filterCrates !== null) {
-                if (this.ALIASES.has(filterCrates)
-                    && this.ALIASES.get(filterCrates).has(lowerQuery)) {
-                    const query_aliases = this.ALIASES.get(filterCrates).get(lowerQuery);
-                    for (const alias of query_aliases) {
-                        aliases.push(alias);
-                    }
+        const innerRunNameQuery =
+            /**
+             * @this {DocSearch}
+             * @param {string} currentCrate
+             * @returns {AsyncGenerator<rustdoc.ResultObject>}
+             */
+            async function*(currentCrate) {
+                const index = this.database.getIndex("normalizedName");
+                if (!index) {
+                    return;
                 }
-            } else {
-                for (const [crate, crateAliasesIndex] of this.ALIASES) {
-                    if (crateAliasesIndex.has(lowerQuery)) {
-                        // @ts-expect-error
-                        const pushTo = crate === currentCrate ? crateAliases : aliases;
-                        const query_aliases = crateAliasesIndex.get(lowerQuery);
-                        for (const alias of query_aliases) {
-                            pushTo.push(alias);
+                const idDuplicates = new Set();
+                const pathDuplicates = new Set();
+                let count = 0;
+                const prefixResults = [];
+                const normalizedUserQuery = parsedQuery.userQuery
+                    .replace(/[_"]/g, "")
+                    .toLowerCase();
+                /**
+                 * @param {string} name
+                 * @param {number} alias
+                 * @param {number} dist
+                 * @param {number} index
+                 * @returns {Promise<rustdoc.PlainResultObject?>}
+                 */
+                const handleAlias = async(name, alias, dist, index) => {
+                    return {
+                        id: alias,
+                        dist,
+                        path_dist: 0,
+                        index,
+                        alias: name,
+                        is_alias: true,
+                        elems: [], // only used in type-based queries
+                        returned: [], // only used in type-based queries
+                        original: await this.getRow(alias),
+                    };
+                };
+                /**
+                 * @param {Promise<rustdoc.PlainResultObject|null>[]} data
+                 * @returns {AsyncGenerator<rustdoc.ResultObject, boolean>}
+                 */
+                const flush = async function* (data) {
+                    const satr = sortAndTransformResults(
+                        await Promise.all(data),
+                        null,
+                        currentCrate,
+                        pathDuplicates,
+                    );
+                    data.length = 0;
+                    for await (const processed of satr) {
+                        yield processed;
+                        count += 1;
+                        if ((count & 0x7F) === 0) {
+                            await yieldToBrowser();
+                        }
+                        if (count >= MAX_RESULTS) {
+                            return true;
                         }
                     }
-                }
-            }
-
-            // @ts-expect-error
-            const sortFunc = (aaa, bbb) => {
-                if (aaa.original.path < bbb.original.path) {
-                    return 1;
-                } else if (aaa.original.path === bbb.original.path) {
-                    return 0;
-                }
-                return -1;
-            };
-            // @ts-expect-error
-            crateAliases.sort(sortFunc);
-            aliases.sort(sortFunc);
-
-            // @ts-expect-error
-            const pushFunc = alias => {
-                // Cloning `alias` to prevent its fields to be updated.
-                alias = {...alias};
-                const res = buildHrefAndPath(alias);
-                alias.displayPath = pathSplitter(res[0]);
-                alias.fullPath = alias.displayPath + alias.name;
-                alias.href = res[1];
-
-                ret.others.unshift(alias);
-                if (ret.others.length > MAX_RESULTS) {
-                    ret.others.pop();
-                }
-            };
-
-            aliases.forEach(pushFunc);
-            // @ts-expect-error
-            crateAliases.forEach(pushFunc);
-        };
-
-        /**
-         * This function adds the given result into the provided `results` map if it matches the
-         * following condition:
-         *
-         * * If it is a "literal search" (`parsedQuery.literalSearch`), then `dist` must be 0.
-         * * If it is not a "literal search", `dist` must be <= `maxEditDistance`.
-         *
-         * The `results` map contains information which will be used to sort the search results:
-         *
-         * * `fullId` is an `integer`` used as the key of the object we use for the `results` map.
-         * * `id` is the index in the `searchIndex` array for this element.
-         * * `index` is an `integer`` used to sort by the position of the word in the item's name.
-         * * `dist` is the main metric used to sort the search results.
-         * * `path_dist` is zero if a single-component search query is used, otherwise it's the
-         *   distance computed for everything other than the last path component.
-         *
-         * @param {rustdoc.Results} results
-         * @param {number} fullId
-         * @param {number} id
-         * @param {number} index
-         * @param {number} dist
-         * @param {number} path_dist
-         * @param {number} maxEditDistance
-         */
-        function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) {
-            if (dist <= maxEditDistance || index !== -1) {
-                if (results.has(fullId)) {
-                    const result = results.get(fullId);
-                    if (result === undefined || result.dontValidate || result.dist <= dist) {
-                        return;
+                    return false;
+                };
+                const aliasResults = await index.search(normalizedUserQuery);
+                if (aliasResults) {
+                    for (const id of aliasResults.matches().entries()) {
+                        const [name, alias] = await Promise.all([
+                            this.getName(id),
+                            this.getAliasTarget(id),
+                        ]);
+                        if (name !== null &&
+                            alias !== null &&
+                            !idDuplicates.has(id) &&
+                            name.replace(/[_"]/g, "").toLowerCase() === normalizedUserQuery
+                        ) {
+                            prefixResults.push(handleAlias(name, alias, 0, 0));
+                            idDuplicates.add(id);
+                        }
                     }
                 }
-                // @ts-expect-error
-                results.set(fullId, {
-                    id: id,
-                    index: index,
-                    dontValidate: parsedQuery.literalSearch,
-                    dist: dist,
-                    path_dist: path_dist,
-                });
-            }
-        }
-
-        /**
-         * This function is called in case the query has more than one element. In this case, it'll
-         * try to match the items which validates all the elements. For `aa -> bb` will look for
-         * functions which have a parameter `aa` and has `bb` in its returned values.
-         *
-         * @param {rustdoc.Row} row
-         * @param {number} pos      - Position in the `searchIndex`.
-         * @param {rustdoc.Results} results
-         */
-        function handleArgs(row, pos, results) {
-            if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
-                return;
-            }
-            const rowType = row.type;
-            if (!rowType) {
-                return;
-            }
-
-            const tfpDist = compareTypeFingerprints(
-                row.id,
-                parsedQuery.typeFingerprint,
-            );
-            if (tfpDist === null) {
-                return;
-            }
-            // @ts-expect-error
-            if (results.size >= MAX_RESULTS && tfpDist > results.max_dist) {
-                return;
-            }
-
-            // If the result is too "bad", we return false and it ends this search.
-            if (!unifyFunctionTypes(
-                rowType.inputs,
-                parsedQuery.elems,
-                rowType.where_clause,
-                null,
-                // @ts-expect-error
-                mgens => {
-                    return unifyFunctionTypes(
-                        rowType.output,
-                        parsedQuery.returned,
-                        rowType.where_clause,
-                        mgens,
-                        checkTypeMgensForConflict,
-                        0, // unboxing depth
-                    );
-                },
-                0, // unboxing depth
-            )) {
-                return;
-            }
-
-            results.max_dist = Math.max(results.max_dist || 0, tfpDist);
-            addIntoResults(results, row.id, pos, 0, tfpDist, 0, Number.MAX_VALUE);
-        }
-
-        /**
-         * Compare the query fingerprint with the function fingerprint.
-         *
-         * @param {number} fullId - The function
-         * @param {Uint32Array} queryFingerprint - The query
-         * @returns {number|null} - Null if non-match, number if distance
-         *                          This function might return 0!
-         */
-        const compareTypeFingerprints = (fullId, queryFingerprint) => {
-            const fh0 = this.functionTypeFingerprint[fullId * 4];
-            const fh1 = this.functionTypeFingerprint[(fullId * 4) + 1];
-            const fh2 = this.functionTypeFingerprint[(fullId * 4) + 2];
-            const [qh0, qh1, qh2] = queryFingerprint;
-            // Approximate set intersection with bloom filters.
-            // This can be larger than reality, not smaller, because hashes have
-            // the property that if they've got the same value, they hash to the
-            // same thing. False positives exist, but not false negatives.
-            const [in0, in1, in2] = [fh0 & qh0, fh1 & qh1, fh2 & qh2];
-            // Approximate the set of items in the query but not the function.
-            // This might be smaller than reality, but cannot be bigger.
-            //
-            // | in_ | qh_ | XOR | Meaning                                          |
-            // | --- | --- | --- | ------------------------------------------------ |
-            // |  0  |  0  |  0  | Not present                                      |
-            // |  1  |  0  |  1  | IMPOSSIBLE because `in_` is `fh_ & qh_`          |
-            // |  1  |  1  |  0  | If one or both is false positive, false negative |
-            // |  0  |  1  |  1  | Since in_ has no false negatives, must be real   |
-            if ((in0 ^ qh0) || (in1 ^ qh1) || (in2 ^ qh2)) {
-                return null;
-            }
-            return this.functionTypeFingerprint[(fullId * 4) + 3];
-        };
-
-
-        const innerRunQuery = () => {
-            if (parsedQuery.foundElems === 1 && !parsedQuery.hasReturnArrow) {
+                if (parsedQuery.error !== null || parsedQuery.elems.length === 0) {
+                    yield* flush(prefixResults);
+                    return;
+                }
                 const elem = parsedQuery.elems[0];
-                // use arrow functions to preserve `this`.
-                /** @type {function(number): void} */
-                const handleNameSearch = id => {
-                    const row = this.searchIndex[id];
-                    if (!typePassesFilter(elem.typeFilter, row.ty) ||
+                const typeFilter = itemTypeFromName(elem.typeFilter);
+                /**
+                 * @param {number} id
+                 * @returns {Promise<rustdoc.PlainResultObject?>}
+                 */
+                const handleNameSearch = async id => {
+                    const row = await this.getRow(id);
+                    if (!row || !row.entry) {
+                        return null;
+                    }
+                    if (!typePassesFilter(typeFilter, row.ty) ||
                         (filterCrates !== null && row.crate !== filterCrates)) {
-                        return;
+                        return null;
                     }
 
+                    /** @type {number|null} */
                     let pathDist = 0;
                     if (elem.fullPath.length > 1) {
-
-                        const maybePathDist = checkPath(elem.pathWithoutLast, row);
-                        if (maybePathDist === null) {
-                            return;
+                        pathDist = checkRowPath(elem.pathWithoutLast, row);
+                        if (pathDist === null) {
+                            return null;
                         }
-                        pathDist = maybePathDist;
                     }
 
                     if (parsedQuery.literalSearch) {
-                        if (row.word === elem.pathLast) {
-                            addIntoResults(results_others, row.id, id, 0, 0, pathDist, 0);
-                        }
+                        return row.name.toLowerCase() === elem.pathLast ? {
+                            id,
+                            dist: 0,
+                            path_dist: 0,
+                            index: 0,
+                            elems: [], // only used in type-based queries
+                            returned: [], // only used in type-based queries
+                            is_alias: false,
+                        } : null;
                     } else {
-                        addIntoResults(
-                            results_others,
-                            row.id,
+                        return {
                             id,
-                            row.normalizedName.indexOf(elem.normalizedPathLast),
-                            editDistance(
+                            dist: editDistance(
                                 row.normalizedName,
                                 elem.normalizedPathLast,
                                 maxEditDistance,
                             ),
-                            pathDist,
-                            maxEditDistance,
-                        );
+                            path_dist: pathDist,
+                            index: row.normalizedName.indexOf(elem.normalizedPathLast),
+                            elems: [], // only used in type-based queries
+                            returned: [], // only used in type-based queries
+                            is_alias: false,
+                        };
                     }
                 };
-                if (elem.normalizedPathLast !== "") {
-                    const last = elem.normalizedPathLast;
-                    for (const id of this.nameTrie.search(last, this.tailTable)) {
-                        handleNameSearch(id);
+                if (elem.normalizedPathLast === "") {
+                    // faster full-table scan for this specific case.
+                    const nameData = this.database.getData("name");
+                    const l = nameData ? nameData.length : 0;
+                    for (let id = 0; id < l; ++id) {
+                        if (!idDuplicates.has(id)) {
+                            idDuplicates.add(id);
+                            prefixResults.push(handleNameSearch(id));
+                        }
+                        if (yield* flush(prefixResults)) {
+                            return;
+                        }
                     }
+                    return;
                 }
-                const length = this.searchIndex.length;
-
-                for (let i = 0, nSearchIndex = length; i < nSearchIndex; ++i) {
-                    // queries that end in :: bypass the trie
-                    if (elem.normalizedPathLast === "") {
-                        handleNameSearch(i);
+                const results = await index.search(elem.normalizedPathLast);
+                if (results) {
+                    for await (const result of results.prefixMatches()) {
+                        for (const id of result.entries()) {
+                            if (!idDuplicates.has(id)) {
+                                idDuplicates.add(id);
+                                prefixResults.push(handleNameSearch(id));
+                                const [name, alias] = await Promise.all([
+                                    this.getName(id),
+                                    this.getAliasTarget(id),
+                                ]);
+                                if (name !== null && alias !== null) {
+                                    prefixResults.push(handleAlias(name, alias, 0, 0));
+                                }
+                            }
+                        }
+                        if (yield* flush(prefixResults)) {
+                            return;
+                        }
                     }
-                    const row = this.searchIndex[i];
-                    if (filterCrates !== null && row.crate !== filterCrates) {
-                        continue;
+                    if (yield* flush(prefixResults)) {
+                        return;
                     }
-                    const tfpDist = compareTypeFingerprints(
-                        row.id,
-                        parsedQuery.typeFingerprint,
-                    );
-                    if (tfpDist !== null) {
-                        const in_args = row.type && row.type.inputs
-                            && checkIfInList(row.type.inputs, elem, row.type.where_clause, null, 0);
-                        const returned = row.type && row.type.output
-                            && checkIfInList(row.type.output, elem, row.type.where_clause, null, 0);
-                        if (in_args) {
-                            results_in_args.max_dist = Math.max(
-                                results_in_args.max_dist || 0,
-                                tfpDist,
-                            );
-                            const maxDist = results_in_args.size < MAX_RESULTS ?
-                                (tfpDist + 1) :
-                                results_in_args.max_dist;
-                            addIntoResults(results_in_args, row.id, i, -1, tfpDist, 0, maxDist);
-                        }
-                        if (returned) {
-                            results_returned.max_dist = Math.max(
-                                results_returned.max_dist || 0,
-                                tfpDist,
-                            );
-                            const maxDist = results_returned.size < MAX_RESULTS ?
-                                (tfpDist + 1) :
-                                results_returned.max_dist;
-                            addIntoResults(results_returned, row.id, i, -1, tfpDist, 0, maxDist);
+                }
+                const levSearchResults = index.searchLev(elem.normalizedPathLast);
+                const levResults = [];
+                for await (const levResult of levSearchResults) {
+                    for (const id of levResult.matches().entries()) {
+                        if (!idDuplicates.has(id)) {
+                            idDuplicates.add(id);
+                            levResults.push(handleNameSearch(id));
+                            const [name, alias] = await Promise.all([
+                                this.getName(id),
+                                this.getAliasTarget(id),
+                            ]);
+                            if (name !== null && alias !== null) {
+                                levResults.push(handleAlias(
+                                    name,
+                                    alias,
+                                    editDistance(elem.normalizedPathLast, name, maxEditDistance),
+                                    name.indexOf(elem.normalizedPathLast),
+                                ));
+                            }
                         }
                     }
                 }
-            } else if (parsedQuery.foundElems > 0) {
-                // Sort input and output so that generic type variables go first and
-                // types with generic parameters go last.
-                // That's because of the way unification is structured: it eats off
-                // the end, and hits a fast path if the last item is a simple atom.
-                /** @type {function(rustdoc.QueryElement, rustdoc.QueryElement): number} */
-                const sortQ = (a, b) => {
-                    const ag = a.generics.length === 0 && a.bindings.size === 0;
-                    const bg = b.generics.length === 0 && b.bindings.size === 0;
-                    if (ag !== bg) {
-                        // unary `+` converts booleans into integers.
-                        return +ag - +bg;
+                yield* flush(levResults);
+                if (results) {
+                    const substringResults = [];
+                    for await (const result of results.substringMatches()) {
+                        for (const id of result.entries()) {
+                            if (!idDuplicates.has(id)) {
+                                idDuplicates.add(id);
+                                substringResults.push(handleNameSearch(id));
+                                const [name, alias] = await Promise.all([
+                                    this.getName(id),
+                                    this.getAliasTarget(id),
+                                ]);
+                                if (name !== null && alias !== null) {
+                                    levResults.push(handleAlias(
+                                        name,
+                                        alias,
+                                        editDistance(
+                                            elem.normalizedPathLast,
+                                            name,
+                                            maxEditDistance,
+                                        ),
+                                        name.indexOf(elem.normalizedPathLast),
+                                    ));
+                                }
+                            }
+                        }
+                        if (yield* flush(substringResults)) {
+                            return;
+                        }
                     }
-                    const ai = a.id !== null && a.id > 0;
-                    const bi = b.id !== null && b.id > 0;
-                    return +ai - +bi;
-                };
-                parsedQuery.elems.sort(sortQ);
-                parsedQuery.returned.sort(sortQ);
-                for (let i = 0, nSearchIndex = this.searchIndex.length; i < nSearchIndex; ++i) {
-                    handleArgs(this.searchIndex[i], i, results_others);
                 }
             }
-        };
+            .bind(this);
 
-        if (parsedQuery.error === null) {
-            innerRunQuery();
-        }
+        const innerRunTypeQuery =
+            /**
+             * @this {DocSearch}
+             * @param {rustdoc.ParserQueryElement[]} inputs
+             * @param {rustdoc.ParserQueryElement[]} output
+             * @param {"sig"|"elems"|"returned"|null} typeInfo
+             * @param {string} currentCrate
+             * @returns {AsyncGenerator<rustdoc.ResultObject>}
+             */
+            async function*(inputs, output, typeInfo, currentCrate) {
+                const index = this.database.getIndex("normalizedName");
+                if (!index) {
+                    return;
+                }
+                /** @type {Map<string, number>} */
+                const genericMap = new Map();
+                /**
+                 * @template Q
+                 * @typedef {{
+                 *     invertedIndex: stringdex.RoaringBitmap[],
+                 *     queryElem: Q,
+                 * }} PostingsList
+                 */
+                /** @type {stringdex.RoaringBitmap[]} */
+                const empty_inverted_index = [];
+                /** @type {PostingsList<any>[]} */
+                const empty_postings_list = [];
+                /** @type {stringdex.RoaringBitmap[]} */
+                const everything_inverted_index = [];
+                for (let i = 0; i < 64; ++i) {
+                    everything_inverted_index.push(RoaringBitmap.everything());
+                }
+                /**
+                 * @type {PostingsList<rustdoc.QueryElement[]>}
+                 */
+                const everything_postings_list = {
+                    invertedIndex: everything_inverted_index,
+                    queryElem: [],
+                };
+                /**
+                 * @type {PostingsList<rustdoc.QueryElement[]>[]}
+                 */
+                const nested_everything_postings_list = [everything_postings_list];
+                /**
+                 * @param {...stringdex.RoaringBitmap[]} idx
+                 * @returns {stringdex.RoaringBitmap[]}
+                 */
+                const intersectInvertedIndexes = (...idx) => {
+                    let i = 0;
+                    const l = idx.length;
+                    while (i < l - 1 && idx[i] === everything_inverted_index) {
+                        i += 1;
+                    }
+                    const result = [...idx[i]];
+                    for (; i < l; ++i) {
+                        if (idx[i] === everything_inverted_index) {
+                            continue;
+                        }
+                        if (idx[i].length < result.length) {
+                            result.length = idx[i].length;
+                        }
+                        for (let j = 0; j < result.length; ++j) {
+                            result[j] = result[j].intersection(idx[i][j]);
+                        }
+                    }
+                    return result;
+                };
+                /**
+                 * Fetch a bitmap of potentially-matching functions,
+                 * plus a list of query elements annotated with the correct IDs.
+                 *
+                 * More than one ID can exist because, for example, q=`Iter` can match
+                 * `std::vec::Iter`, or `std::btree_set::Iter`, or anything else, and those
+                 * items different IDs. What's worse, q=`Iter<Iter>` has N**2 possible
+                 * matches, because it could be `vec::Iter<btree_set::Iter>`,
+                 * `btree_set::Iter<vec::Iter>`, `vec::Iter<vec::Iter>`,
+                 * `btree_set::Iter<btree_set::Iter>`,
+                 * or anything else. This function returns all possible permutations.
+                 *
+                 * @param {rustdoc.ParserQueryElement|null} elem
+                 * @returns {Promise<PostingsList<rustdoc.QueryElement>[]>}
+                 */
+                const unpackPostingsList = async elem => {
+                    if (!elem) {
+                        return empty_postings_list;
+                    }
+                    const typeFilter = itemTypeFromName(elem.typeFilter);
+                    const searchResults = await index.search(elem.normalizedPathLast);
+                    /**
+                     * @type {Promise<[
+                     *     number,
+                     *     string|null,
+                     *     rustdoc.TypeData|null,
+                     *     rustdoc.PathData|null,
+                     * ]>[]}
+                     * */
+                    const typePromises = [];
+                    if (typeFilter !== TY_GENERIC && searchResults) {
+                        for (const id of searchResults.matches().entries()) {
+                            typePromises.push(Promise.all([
+                                this.getName(id),
+                                this.getTypeData(id),
+                                this.getPathData(id),
+                            ]).then(([name, typeData, pathData]) =>
+                                [id, name, typeData, pathData]));
+                        }
+                    }
+                    const types = (await Promise.all(typePromises))
+                        .filter(([_id, name, ty, path]) =>
+                            name !== null && name.toLowerCase() === elem.pathLast &&
+                            ty && !ty.invertedFunctionSignatureIndex.every(bitmap => {
+                                return bitmap.isEmpty();
+                            }) &&
+                            path && path.ty !== TY_ASSOCTYPE &&
+                            (elem.pathWithoutLast.length === 0 ||
+                                checkPath(
+                                    elem.pathWithoutLast,
+                                    path.modulePath.split("::"),
+                                ) === 0),
+                            );
+                    if (types.length === 0) {
+                        const areGenericsAllowed = typeFilter === TY_GENERIC || (
+                            typeFilter === -1 &&
+                            (parsedQuery.totalElems > 1 || parsedQuery.hasReturnArrow) &&
+                            elem.pathWithoutLast.length === 0 &&
+                            elem.generics.length === 0 &&
+                            elem.bindings.size === 0
+                        );
+                        if (typeFilter !== TY_GENERIC &&
+                            (elem.name.length >= 3 || !areGenericsAllowed)
+                        ) {
+                            /** @type {string|null} */
+                            let chosenName = null;
+                            /** @type {rustdoc.TypeData[]} */
+                            let chosenType = [];
+                            /** @type {rustdoc.PathData[]} */
+                            let chosenPath = [];
+                            /** @type {number[]} */
+                            let chosenId = [];
+                            let chosenDist = Number.MAX_SAFE_INTEGER;
+                            const levResults = index.searchLev(elem.normalizedPathLast);
+                            for await (const searchResults of levResults) {
+                                for (const id of searchResults.matches().entries()) {
+                                    const [name, ty, path] = await Promise.all([
+                                        this.getName(id),
+                                        this.getTypeData(id),
+                                        this.getPathData(id),
+                                    ]);
+                                    if (name !== null && ty !== null && path !== null &&
+                                        !ty.invertedFunctionSignatureIndex.every(bitmap => {
+                                            return bitmap.isEmpty();
+                                        }) &&
+                                        path.ty !== TY_ASSOCTYPE
+                                    ) {
+                                        let dist = editDistance(
+                                            name,
+                                            elem.pathLast,
+                                            maxEditDistance,
+                                        );
+                                        if (elem.pathWithoutLast.length !== 0) {
+                                            const pathDist = checkPath(
+                                                elem.pathWithoutLast,
+                                                path.modulePath.split("::"),
+                                            );
+                                            // guaranteed to be higher than the path limit
+                                            dist += pathDist === null ?
+                                                Number.MAX_SAFE_INTEGER :
+                                                pathDist;
+                                        }
+                                        if (name === chosenName) {
+                                            chosenId.push(id);
+                                            chosenType.push(ty);
+                                            chosenPath.push(path);
+                                        } else if (dist < chosenDist) {
+                                            chosenName = name;
+                                            chosenId = [id];
+                                            chosenType = [ty];
+                                            chosenPath = [path];
+                                            chosenDist = dist;
+                                        }
+                                    }
+                                }
+                                if (chosenId.length !== 0) {
+                                    // searchLev returns results in order
+                                    // if we have working matches, we're done
+                                    break;
+                                }
+                            }
+                            if (areGenericsAllowed) {
+                                parsedQuery.proposeCorrectionFrom = elem.name;
+                                parsedQuery.proposeCorrectionTo = chosenName;
+                            } else {
+                                parsedQuery.correction = chosenName;
+                                for (let i = 0; i < chosenType.length; ++i) {
+                                    types.push([
+                                        chosenId[i],
+                                        chosenName,
+                                        chosenType[i],
+                                        chosenPath[i],
+                                    ]);
+                                }
+                            }
+                        }
+                        if (areGenericsAllowed) {
+                            let genericId = genericMap.get(elem.normalizedPathLast);
+                            if (genericId === undefined) {
+                                genericId = genericMap.size;
+                                genericMap.set(elem.normalizedPathLast, genericId);
+                            }
+                            return [{
+                                invertedIndex: await this.getGenericInvertedIndex(genericId),
+                                queryElem: {
+                                    name: elem.name,
+                                    id: (-genericId) - 1,
+                                    typeFilter: TY_GENERIC,
+                                    generics: [],
+                                    bindings: EMPTY_BINDINGS_MAP,
+                                    fullPath: elem.fullPath,
+                                    pathLast: elem.pathLast,
+                                    normalizedPathLast: elem.normalizedPathLast,
+                                    pathWithoutLast: elem.pathWithoutLast,
+                                },
+                            }];
+                        }
+                    }
+                    types.sort(([_i, name1, _t, pathData1], [_i2, name2, _t2, pathData2]) => {
+                        const p1 = !pathData1 ? "" : pathData1.modulePath;
+                        const p2 = !pathData2 ? "" : pathData2.modulePath;
+                        const n1 = name1 === null ? "" : name1;
+                        const n2 = name2 === null ? "" : name2;
+                        if (p1.length !== p2.length) {
+                            return p1.length > p2.length ? +1 : -1;
+                        }
+                        if (n1.length !== n2.length) {
+                            return n1.length > n2.length ? +1 : -1;
+                        }
+                        if (n1 !== n2) {
+                            return n1 > n2 ? +1 : -1;
+                        }
+                        if (p1 !== p2) {
+                            return p1 > p2 ? +1 : -1;
+                        }
+                        return 0;
+                    });
+                    /** @type {PostingsList<rustdoc.QueryElement>[]} */
+                    const results = [];
+                    for (const [id, _name, typeData] of types) {
+                        if (!typeData || typeData.invertedFunctionSignatureIndex.every(bitmap => {
+                            return bitmap.isEmpty();
+                        })) {
+                            continue;
+                        }
+                        const upla = await unpackPostingsListAll(elem.generics);
+                        const uplb = await unpackPostingsListBindings(elem.bindings);
+                        for (const {invertedIndex: genericsIdx, queryElem: generics} of upla) {
+                            for (const {invertedIndex: bindingsIdx, queryElem: bindings} of uplb) {
+                                results.push({
+                                    invertedIndex: intersectInvertedIndexes(
+                                        typeData.invertedFunctionSignatureIndex,
+                                        genericsIdx,
+                                        bindingsIdx,
+                                    ),
+                                    queryElem: {
+                                        name: elem.name,
+                                        id,
+                                        typeFilter,
+                                        generics,
+                                        bindings,
+                                        fullPath: elem.fullPath,
+                                        pathLast: elem.pathLast,
+                                        normalizedPathLast: elem.normalizedPathLast,
+                                        pathWithoutLast: elem.pathWithoutLast,
+                                    },
+                                });
+                                if ((results.length & 0x7F) === 0) {
+                                    await yieldToBrowser();
+                                }
+                            }
+                        }
+                    }
+                    return results;
+                };
+                /**
+                 * Fetch all possible matching permutations of a list of query elements.
+                 *
+                 * The empty list returns an "identity postings list", with a bitmap that
+                 * matches everything and an empty list of elems. This allows you to safely
+                 * take the intersection of this bitmap.
+                 *
+                 * @param {(rustdoc.ParserQueryElement|null)[]|null} elems
+                 * @returns {Promise<PostingsList<rustdoc.QueryElement[]>[]>}
+                 */
+                const unpackPostingsListAll = async elems => {
+                    if (!elems || elems.length === 0) {
+                        return nested_everything_postings_list;
+                    }
+                    const [firstPostingsList, remainingAll] = await Promise.all([
+                        unpackPostingsList(elems[0]),
+                        unpackPostingsListAll(elems.slice(1)),
+                    ]);
+                    /** @type {PostingsList<rustdoc.QueryElement[]>[]} */
+                    const results = [];
+                    for (const {
+                        invertedIndex: firstIdx,
+                        queryElem: firstElem,
+                    } of firstPostingsList) {
+                        for (const {
+                            invertedIndex: remainingIdx,
+                            queryElem: remainingElems,
+                        } of remainingAll) {
+                            results.push({
+                                invertedIndex: intersectInvertedIndexes(firstIdx, remainingIdx),
+                                queryElem: [firstElem, ...remainingElems],
+                            });
+                            if ((results.length & 0x7F) === 0) {
+                                await yieldToBrowser();
+                            }
+                        }
+                    }
+                    return results;
+                };
+                /**
+                 * Fetch all possible matching permutations of a map query element bindings.
+                 *
+                 * The empty list returns an "identity postings list", with a bitmap that
+                 * matches everything and an empty list of elems. This allows you to safely
+                 * take the intersection of this bitmap.
+                 *
+                 * Heads up! This function mutates the Map that you provide.
+                 * Before passing an actual parser item to it, make sure to clone the map.
+                 *
+                 * @param {Map<string, rustdoc.ParserQueryElement[]>} elems
+                 * @returns {Promise<PostingsList<
+                 *     Map<number, rustdoc.QueryElement[]>,
+                 * >[]>}
+                 */
+                const unpackPostingsListBindings = async elems => {
+                    if (!elems) {
+                        return [{
+                            invertedIndex: everything_inverted_index,
+                            queryElem: new Map(),
+                        }];
+                    }
+                    const firstKey = elems.keys().next().value;
+                    if (firstKey === undefined) {
+                        return [{
+                            invertedIndex: everything_inverted_index,
+                            queryElem: new Map(),
+                        }];
+                    }
+                    const firstList = elems.get(firstKey);
+                    if (firstList === undefined) {
+                        return [{
+                            invertedIndex: everything_inverted_index,
+                            queryElem: new Map(),
+                        }];
+                    }
+                    const firstKeyIds = await index.search(firstKey);
+                    if (!firstKeyIds) {
+                        // User specified a non-existent key.
+                        return [{
+                            invertedIndex: empty_inverted_index,
+                            queryElem: new Map(),
+                        }];
+                    }
+                    elems.delete(firstKey);
+                    const [firstPostingsList, remainingAll] = await Promise.all([
+                        unpackPostingsListAll(firstList),
+                        unpackPostingsListBindings(elems),
+                    ]);
+                    /** @type {PostingsList<Map<number, rustdoc.QueryElement[]>>[]} */
+                    const results = [];
+                    for (const keyId of firstKeyIds.matches().entries()) {
+                        for (const {
+                            invertedIndex: firstIdx,
+                            queryElem: firstElem,
+                        } of firstPostingsList) {
+                            for (const {
+                                invertedIndex: remainingIdx,
+                                queryElem: remainingElems,
+                            } of remainingAll) {
+                                const elems = new Map(remainingElems);
+                                elems.set(keyId, firstElem);
+                                results.push({
+                                    invertedIndex: intersectInvertedIndexes(firstIdx, remainingIdx),
+                                    queryElem: elems,
+                                });
+                                if ((results.length & 0x7F) === 0) {
+                                    await yieldToBrowser();
+                                }
+                            }
+                        }
+                    }
+                    elems.set(firstKey, firstList);
+                    if (results.length === 0) {
+                        // User specified a non-existent key.
+                        return [{
+                            invertedIndex: empty_inverted_index,
+                            queryElem: new Map(),
+                        }];
+                    }
+                    return results;
+                };
 
-        const isType = parsedQuery.foundElems !== 1 || parsedQuery.hasReturnArrow;
-        const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([
-            sortResults(results_in_args, "elems", currentCrate),
-            sortResults(results_returned, "returned", currentCrate),
-            // @ts-expect-error
-            sortResults(results_others, (isType ? "query" : null), currentCrate),
-        ]);
-        const ret = createQueryResults(
-            sorted_in_args,
-            sorted_returned,
-            sorted_others,
-            parsedQuery);
-        await handleAliases(ret, parsedQuery.userQuery.replace(/"/g, ""),
-            filterCrates, currentCrate);
-        await Promise.all([ret.others, ret.returned, ret.in_args].map(async list => {
-            const descs = await Promise.all(list.map(result => {
-                // @ts-expect-error
-                return this.searchIndexEmptyDesc.get(result.crate).contains(result.bitIndex) ?
-                    "" :
-                    // @ts-expect-error
-                    this.searchState.loadDesc(result);
-            }));
-            for (const [i, result] of list.entries()) {
-                // @ts-expect-error
-                result.desc = descs[i];
+                // finally, we can do the actual unification loop
+                const [allInputs, allOutput] = await Promise.all([
+                    unpackPostingsListAll(inputs),
+                    unpackPostingsListAll(output),
+                ]);
+                let checkCounter = 0;
+                /**
+                 * Finally, we can perform an incremental search, sorted by the number of
+                 * entries that match a given query.
+                 *
+                 * The outer list gives the number of elements. The inner one is separate
+                 * for each distinct name resolution.
+                 *
+                 * @type {{
+                 *     bitmap: stringdex.RoaringBitmap,
+                 *     inputs: rustdoc.QueryElement[],
+                 *     output: rustdoc.QueryElement[],
+                 * }[][]}
+                 */
+                const queryPlan = [];
+                for (const {invertedIndex: inputsIdx, queryElem: inputs} of allInputs) {
+                    for (const {invertedIndex: outputIdx, queryElem: output} of allOutput) {
+                        const invertedIndex = intersectInvertedIndexes(inputsIdx, outputIdx);
+                        for (const [size, bitmap] of invertedIndex.entries()) {
+                            checkCounter += 1;
+                            if ((checkCounter & 0x7F) === 0) {
+                                await yieldToBrowser();
+                            }
+                            if (!queryPlan[size]) {
+                                queryPlan[size] = [];
+                            }
+                            queryPlan[size].push({
+                                bitmap, inputs, output,
+                            });
+                        }
+                    }
+                }
+                const resultPromises = [];
+                const dedup = new Set();
+                let resultCounter = 0;
+                const isReturnTypeQuery = inputs.length === 0;
+                /** @type {rustdoc.PlainResultObject[]} */
+                const pushToBottom = [];
+                plan: for (const queryStep of queryPlan) {
+                    for (const {bitmap, inputs, output} of queryStep) {
+                        for (const id of bitmap.entries()) {
+                            checkCounter += 1;
+                            if ((checkCounter & 0x7F) === 0) {
+                                await yieldToBrowser();
+                            }
+                            resultPromises.push(this.getFunctionData(id).then(async fnData => {
+                                if (!fnData || !fnData.functionSignature) {
+                                    return null;
+                                }
+                                checkCounter += 1;
+                                if ((checkCounter & 0x7F) === 0) {
+                                    await yieldToBrowser();
+                                }
+                                const functionSignature = fnData.functionSignature;
+                                if (!unifyFunctionTypes(
+                                    functionSignature.inputs,
+                                    inputs,
+                                    functionSignature.where_clause,
+                                    null,
+                                    mgens => {
+                                        return !!unifyFunctionTypes(
+                                            functionSignature.output,
+                                            output,
+                                            functionSignature.where_clause,
+                                            mgens,
+                                            checkTypeMgensForConflict,
+                                            0, // unboxing depth
+                                        );
+                                    },
+                                    0, // unboxing depth
+                                )) {
+                                    return null;
+                                }
+                                const result = {
+                                    id,
+                                    dist: fnData.elemCount,
+                                    path_dist: 0,
+                                    index: -1,
+                                    elems: inputs,
+                                    returned: output,
+                                    is_alias: false,
+                                };
+                                const entry = await this.getEntryData(id);
+                                if ((entry && !isFnLikeTy(entry.ty)) ||
+                                    (isReturnTypeQuery &&
+                                        functionSignature &&
+                                        containsTypeFromQuery(
+                                            output,
+                                            functionSignature.inputs,
+                                            functionSignature.where_clause,
+                                        )
+                                    )
+                                ) {
+                                    pushToBottom.push(result);
+                                    return null;
+                                }
+                                return result;
+                            }));
+                        }
+                    }
+                    for await (const result of sortAndTransformResults(
+                        await Promise.all(resultPromises),
+                        typeInfo,
+                        currentCrate,
+                        dedup,
+                    )) {
+                        if (resultCounter >= MAX_RESULTS) {
+                            break plan;
+                        }
+                        yield result;
+                        resultCounter += 1;
+                    }
+                    resultPromises.length = 0;
+                }
+                if (resultCounter >= MAX_RESULTS) {
+                    return;
+                }
+                for await (const result of sortAndTransformResults(
+                    await Promise.all(pushToBottom),
+                    typeInfo,
+                    currentCrate,
+                    dedup,
+                )) {
+                    if (resultCounter >= MAX_RESULTS) {
+                        break;
+                    }
+                    yield result;
+                    resultCounter += 1;
+                }
             }
-        }));
-        if (parsedQuery.error !== null && ret.others.length !== 0) {
-            // It means some doc aliases were found so let's "remove" the error!
-            ret.query.error = null;
+            .bind(this);
+
+        if (parsedQuery.foundElems === 1 && !parsedQuery.hasReturnArrow) {
+            // We never want the main tab to delay behind the other two tabs.
+            // This is a bit of a hack (because JS's scheduler doesn't have much of an API),
+            // along with making innerRunTypeQuery yield to the UI thread.
+            const {
+                promise: donePromise,
+                resolve: doneResolve,
+                reject: doneReject,
+            } = Promise.withResolvers();
+            const doneTimeout = timeout(250);
+            return {
+                "in_args": (async function*() {
+                    await Promise.race([donePromise, doneTimeout]);
+                    yield* innerRunTypeQuery(parsedQuery.elems, [], "elems", currentCrate);
+                })(),
+                "returned": (async function*() {
+                    await Promise.race([donePromise, doneTimeout]);
+                    yield* innerRunTypeQuery([], parsedQuery.elems, "returned", currentCrate);
+                })(),
+                "others": (async function*() {
+                    try {
+                        yield* innerRunNameQuery(currentCrate);
+                        doneResolve(null);
+                    } catch (e) {
+                        doneReject(e);
+                        throw e;
+                    }
+                })(),
+                "query": parsedQuery,
+            };
+        } else if (parsedQuery.error !== null) {
+            return {
+                "in_args": (async function*() {})(),
+                "returned": (async function*() {})(),
+                "others": innerRunNameQuery(currentCrate),
+                "query": parsedQuery,
+            };
+        } else {
+            const typeInfo = parsedQuery.elems.length === 0 ?
+                "returned" : (
+                    parsedQuery.returned.length === 0 ? "elems" : "sig"
+                );
+            return {
+                "in_args": (async function*() {})(),
+                "returned": (async function*() {})(),
+                "others": parsedQuery.foundElems === 0 ?
+                    (async function*() {})() :
+                    innerRunTypeQuery(
+                        parsedQuery.elems,
+                        parsedQuery.returned,
+                        typeInfo,
+                        currentCrate,
+                    ),
+                "query": parsedQuery,
+            };
         }
-        return ret;
     }
 }
 
 
 // ==================== Core search logic end ====================
 
-/** @type {Map<string, rustdoc.RawSearchIndexCrate>} */
-let rawSearchIndex;
-// @ts-expect-error
+/** @type {DocSearch} */
 let docSearch;
 const longItemTypes = [
     "keyword",
@@ -4787,12 +4631,8 @@ function buildUrl(search, filterCrates) {
 function getFilterCrates() {
     const elem = document.getElementById("crate-search");
 
-    if (elem &&
-        // @ts-expect-error
-        elem.value !== "all crates" &&
-        // @ts-expect-error
-        window.searchIndex.has(elem.value)
-    ) {
+    // @ts-expect-error
+    if (elem && elem.value !== "all crates") {
         // @ts-expect-error
         return elem.value;
     }
@@ -4802,8 +4642,7 @@ function getFilterCrates() {
 // @ts-expect-error
 function nextTab(direction) {
     const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length;
-    // @ts-expect-error
-    searchState.focusedByTab[searchState.currentTab] = document.activeElement;
+    window.searchState.focusedByTab[searchState.currentTab] = document.activeElement;
     printTab(next);
     focusSearchResult();
 }
@@ -4815,133 +4654,182 @@ function focusSearchResult() {
         document.querySelectorAll(".search-results.active a").item(0) ||
         document.querySelectorAll("#search-tabs button").item(searchState.currentTab);
     searchState.focusedByTab[searchState.currentTab] = null;
-    if (target) {
-        // @ts-expect-error
+    if (target && target instanceof HTMLElement) {
         target.focus();
     }
 }
 
 /**
  * Render a set of search results for a single tab.
- * @param {Array<?>}    array   - The search results for this tab
- * @param {rustdoc.ParsedQuery<rustdoc.QueryElement>} query
+ * @param {AsyncGenerator<rustdoc.ResultObject>} results   - The search results for this tab
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
  * @param {boolean}     display - True if this is the active tab
+ * @param {function(number, HTMLElement): any} finishedCallback
+ * @param {boolean} isTypeSearch
+ * @returns {Promise<HTMLElement>}
  */
-async function addTab(array, query, display) {
+async function addTab(results, query, display, finishedCallback, isTypeSearch) {
     const extraClass = display ? " active" : "";
 
-    const output = document.createElement(
-        array.length === 0 && query.error === null ? "div" : "ul",
-    );
-    if (array.length > 0) {
-        output.className = "search-results " + extraClass;
+    /** @type {HTMLElement} */
+    let output = document.createElement("ul");
+    output.className = "search-results " + extraClass;
 
-        const lis = Promise.all(array.map(async item => {
-            const name = item.is_alias ? item.original.name : item.name;
-            const type = itemTypes[item.ty];
-            const longType = longItemTypes[item.ty];
-            const typeName = longType.length !== 0 ? `${longType}` : "?";
+    let count = 0;
 
-            const link = document.createElement("a");
-            link.className = "result-" + type;
-            link.href = item.href;
+    /** @type {Promise<string|null>[]} */
+    const descList = [];
 
-            const resultName = document.createElement("span");
-            resultName.className = "result-name";
+    /** @param {rustdoc.ResultObject} obj */
+    const addNextResultToOutput = async obj => {
+        count += 1;
 
-            resultName.insertAdjacentHTML(
-                "beforeend",
-                `<span class="typename">${typeName}</span>`);
-            link.appendChild(resultName);
+        const name = obj.item.name;
+        const type = itemTypes[obj.item.ty];
+        const longType = longItemTypes[obj.item.ty];
+        const typeName = longType.length !== 0 ? `${longType}` : "?";
 
-            let alias = " ";
-            if (item.is_alias) {
-                alias = ` <div class="alias">\
-<b>${item.name}</b><i class="grey">&nbsp;- see&nbsp;</i>\
+        const link = document.createElement("a");
+        link.className = "result-" + type;
+        link.href = obj.href;
+
+        const resultName = document.createElement("span");
+        resultName.className = "result-name";
+
+        resultName.insertAdjacentHTML(
+            "beforeend",
+            `<span class="typename">${typeName}</span>`);
+        link.appendChild(resultName);
+
+        let alias = " ";
+        if (obj.alias !== undefined) {
+            alias = ` <div class="alias">\
+<b>${obj.alias}</b><i class="grey">&nbsp;- see&nbsp;</i>\
 </div>`;
-            }
-            resultName.insertAdjacentHTML(
-                "beforeend",
-                `<div class="path">${alias}\
-${item.displayPath}<span class="${type}">${name}</span>\
+        }
+        resultName.insertAdjacentHTML(
+            "beforeend",
+            `<div class="path">${alias}\
+${obj.displayPath}<span class="${type}">${name}</span>\
 </div>`);
 
-            const description = document.createElement("div");
-            description.className = "desc";
-            description.insertAdjacentHTML("beforeend", item.desc);
-            if (item.displayTypeSignature) {
-                const {type, mappedNames, whereClause} = await item.displayTypeSignature;
-                const displayType = document.createElement("div");
-                // @ts-expect-error
-                type.forEach((value, index) => {
-                    if (index % 2 !== 0) {
-                        const highlight = document.createElement("strong");
-                        highlight.appendChild(document.createTextNode(value));
-                        displayType.appendChild(highlight);
-                    } else {
-                        displayType.appendChild(document.createTextNode(value));
+        const description = document.createElement("div");
+        description.className = "desc";
+        obj.desc.then(desc => {
+            if (desc !== null) {
+                description.insertAdjacentHTML("beforeend", desc);
+            }
+        });
+        descList.push(obj.desc);
+        if (obj.displayTypeSignature) {
+            const {type, mappedNames, whereClause} = await obj.displayTypeSignature;
+            const displayType = document.createElement("div");
+            type.forEach((value, index) => {
+                if (index % 2 !== 0) {
+                    const highlight = document.createElement("strong");
+                    highlight.appendChild(document.createTextNode(value));
+                    displayType.appendChild(highlight);
+                } else {
+                    displayType.appendChild(document.createTextNode(value));
+                }
+            });
+            if (mappedNames.size > 0 || whereClause.size > 0) {
+                let addWhereLineFn = () => {
+                    const line = document.createElement("div");
+                    line.className = "where";
+                    line.appendChild(document.createTextNode("where"));
+                    displayType.appendChild(line);
+                    addWhereLineFn = () => {};
+                };
+                for (const [qname, name] of mappedNames) {
+                    // don't care unless the generic name is different
+                    if (name === qname) {
+                        continue;
                     }
-                });
-                if (mappedNames.size > 0 || whereClause.size > 0) {
-                    let addWhereLineFn = () => {
-                        const line = document.createElement("div");
-                        line.className = "where";
-                        line.appendChild(document.createTextNode("where"));
-                        displayType.appendChild(line);
-                        addWhereLineFn = () => {};
-                    };
-                    for (const [qname, name] of mappedNames) {
-                        // don't care unless the generic name is different
-                        if (name === qname) {
-                            continue;
-                        }
-                        addWhereLineFn();
-                        const line = document.createElement("div");
-                        line.className = "where";
-                        line.appendChild(document.createTextNode(`    ${qname} matches `));
-                        const lineStrong = document.createElement("strong");
-                        lineStrong.appendChild(document.createTextNode(name));
-                        line.appendChild(lineStrong);
-                        displayType.appendChild(line);
+                    addWhereLineFn();
+                    const line = document.createElement("div");
+                    line.className = "where";
+                    line.appendChild(document.createTextNode(`    ${qname} matches `));
+                    const lineStrong = document.createElement("strong");
+                    lineStrong.appendChild(document.createTextNode(name));
+                    line.appendChild(lineStrong);
+                    displayType.appendChild(line);
+                }
+                for (const [name, innerType] of whereClause) {
+                    // don't care unless there's at least one highlighted entry
+                    if (innerType.length <= 1) {
+                        continue;
                     }
-                    for (const [name, innerType] of whereClause) {
-                        // don't care unless there's at least one highlighted entry
-                        if (innerType.length <= 1) {
-                            continue;
+                    addWhereLineFn();
+                    const line = document.createElement("div");
+                    line.className = "where";
+                    line.appendChild(document.createTextNode(`    ${name}: `));
+                    innerType.forEach((value, index) => {
+                        if (index % 2 !== 0) {
+                            const highlight = document.createElement("strong");
+                            highlight.appendChild(document.createTextNode(value));
+                            line.appendChild(highlight);
+                        } else {
+                            line.appendChild(document.createTextNode(value));
                         }
-                        addWhereLineFn();
-                        const line = document.createElement("div");
-                        line.className = "where";
-                        line.appendChild(document.createTextNode(`    ${name}: `));
-                        // @ts-expect-error
-                        innerType.forEach((value, index) => {
-                            if (index % 2 !== 0) {
-                                const highlight = document.createElement("strong");
-                                highlight.appendChild(document.createTextNode(value));
-                                line.appendChild(highlight);
-                            } else {
-                                line.appendChild(document.createTextNode(value));
-                            }
-                        });
-                        displayType.appendChild(line);
-                    }
+                    });
+                    displayType.appendChild(line);
                 }
-                displayType.className = "type-signature";
-                link.appendChild(displayType);
             }
+            displayType.className = "type-signature";
+            link.appendChild(displayType);
+        }
+
+        link.appendChild(description);
+        output.appendChild(link);
 
-            link.appendChild(description);
-            return link;
-        }));
-        lis.then(lis => {
-            for (const li of lis) {
-                output.appendChild(li);
+        results.next().then(async nextResult => {
+            if (nextResult.value) {
+                addNextResultToOutput(nextResult.value);
+            } else {
+                await Promise.all(descList);
+                // need to make sure the element is shown before
+                // running this callback
+                yieldToBrowser().then(() => finishedCallback(count, output));
             }
         });
-    } else if (query.error === null) {
-        const dlroChannel = `https://doc.rust-lang.org/${getVar("channel")}`;
+    };
+    const firstResult = await results.next();
+    let correctionOutput = "";
+    if (query.correction !== null && isTypeSearch) {
+        const orig = query.returned.length > 0
+            ? query.returned[0].name
+            : query.elems[0].name;
+        correctionOutput = "<h3 class=\"search-corrections\">" +
+            `Type "${orig}" not found. ` +
+            "Showing results for closest type name " +
+            `"${query.correction}" instead.</h3>`;
+    }
+    if (query.proposeCorrectionFrom !== null && isTypeSearch) {
+        const orig = query.proposeCorrectionFrom;
+        const targ = query.proposeCorrectionTo;
+        correctionOutput = "<h3 class=\"search-corrections\">" +
+            `Type "${orig}" not found and used as generic parameter. ` +
+            `Consider searching for "${targ}" instead.</h3>`;
+    }
+    if (firstResult.value) {
+        if (correctionOutput !== "") {
+            const h3 = document.createElement("h3");
+            h3.innerHTML = correctionOutput;
+            output.appendChild(h3);
+        }
+        await addNextResultToOutput(firstResult.value);
+    } else {
+        output = document.createElement("div");
+        if (correctionOutput !== "") {
+            const h3 = document.createElement("h3");
+            h3.innerHTML = correctionOutput;
+            output.appendChild(h3);
+        }
         output.className = "search-failed" + extraClass;
-        output.innerHTML = "No results :(<br/>" +
+        const dlroChannel = `https://doc.rust-lang.org/${getVar("channel")}`;
+        if (query.userQuery !== "") {
+            output.innerHTML += "No results :(<br/>" +
             "Try on <a href=\"https://duckduckgo.com/?q=" +
             encodeURIComponent("rust " + query.userQuery) +
             "\">DuckDuckGo</a>?<br/><br/>" +
@@ -4954,192 +4842,198 @@ ${item.displayPath}<span class="${type}">${name}</span>\
             "introductions to language features and the language itself.</li><li><a " +
             "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" +
             " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>";
+        }
+        output.innerHTML += "Example searches:<ul>" +
+            "<li><a href=\"" + getNakedUrl() + "?search=std::vec\">std::vec</a></li>" +
+            "<li><a href=\"" + getNakedUrl() + "?search=u32+->+bool\">u32 -> bool</a></li>" +
+            "<li><a href=\"" + getNakedUrl() + "?search=Option<T>,+(T+->+U)+->+Option<U>\">" +
+                "Option&lt;T>, (T -> U) -> Option&lt;U></a></li>" +
+            "</ul>";
+        // need to make sure the element is shown before
+        // running this callback
+        yieldToBrowser().then(() => finishedCallback(0, output));
     }
     return output;
 }
 
-// @ts-expect-error
-function makeTabHeader(tabNb, text, nbElems) {
-    // https://blog.horizon-eda.org/misc/2020/02/19/ui.html
-    //
-    // CSS runs with `font-variant-numeric: tabular-nums` to ensure all
-    // digits are the same width. \u{2007} is a Unicode space character
-    // that is defined to be the same width as a digit.
-    const fmtNbElems =
-        nbElems < 10  ? `\u{2007}(${nbElems})\u{2007}\u{2007}` :
-        nbElems < 100 ? `\u{2007}(${nbElems})\u{2007}` : `\u{2007}(${nbElems})`;
-    if (searchState.currentTab === tabNb) {
-        return "<button class=\"selected\">" + text +
-            "<span class=\"count\">" + fmtNbElems + "</span></button>";
-    }
-    return "<button>" + text + "<span class=\"count\">" + fmtNbElems + "</span></button>";
+/**
+ * returns [tab, output]
+ * @param {number} tabNb
+ * @param {string} text
+ * @param {AsyncGenerator<rustdoc.ResultObject>} results
+ * @param {rustdoc.ParsedQuery<rustdoc.ParserQueryElement>} query
+ * @param {boolean} isTypeSearch
+ * @param {boolean} goToFirst
+ * @returns {[HTMLElement, Promise<HTMLElement>]}
+ */
+function makeTab(tabNb, text, results, query, isTypeSearch, goToFirst) {
+    const isCurrentTab = window.searchState.currentTab === tabNb;
+    const tabButton = document.createElement("button");
+    tabButton.appendChild(document.createTextNode(text));
+    tabButton.className = isCurrentTab ? "selected" : "";
+    const tabCount = document.createElement("span");
+    tabCount.className = "count loading";
+    tabCount.innerHTML = "\u{2007}(\u{2007})\u{2007}\u{2007}";
+    tabButton.appendChild(tabCount);
+    return [
+        tabButton,
+        addTab(results, query, isCurrentTab, (count, output) => {
+            const search = window.searchState.outputElement();
+            const error = query.error;
+            if (count === 0 && error !== null && search) {
+                error.forEach((value, index) => {
+                    value = value.split("<").join("&lt;").split(">").join("&gt;");
+                    if (index % 2 !== 0) {
+                        error[index] = `<code>${value.replaceAll(" ", "&nbsp;")}</code>`;
+                    } else {
+                        error[index] = value;
+                    }
+                });
+                const errorReport = document.createElement("h3");
+                errorReport.className = "error";
+                errorReport.innerHTML = `Query parser error: "${error.join("")}".`;
+                search.insertBefore(errorReport, search.firstElementChild);
+            } else if (goToFirst ||
+                (count === 1 && getSettingValue("go-to-only-result") === "true")
+            ) {
+                // Needed to force re-execution of JS when coming back to a page. Let's take this
+                // scenario as example:
+                //
+                // 1. You have the "Directly go to item in search if there is only one result"
+                //    option enabled.
+                // 2. You make a search which results only one result, leading you automatically to
+                //    this result.
+                // 3. You go back to previous page.
+                //
+                // Now, without the call below, the JS will not be re-executed and the previous
+                // state will be used, starting search again since the search input is not empty,
+                // leading you back to the previous page again.
+                window.onunload = () => { };
+                window.searchState.removeQueryParameters();
+                const a = output.querySelector("a");
+                if (a) {
+                    a.click();
+                    return;
+                }
+            }
+
+            // https://blog.horizon-eda.org/misc/2020/02/19/ui.html
+            //
+            // CSS runs with `font-variant-numeric: tabular-nums` to ensure all
+            // digits are the same width. \u{2007} is a Unicode space character
+            // that is defined to be the same width as a digit.
+            const fmtNbElems =
+                count < 10  ? `\u{2007}(${count})\u{2007}\u{2007}` :
+                count < 100 ? `\u{2007}(${count})\u{2007}` : `\u{2007}(${count})`;
+            tabCount.innerHTML = fmtNbElems;
+            tabCount.className = "count";
+        }, isTypeSearch),
+    ];
 }
 
 /**
+ * @param {DocSearch} docSearch
  * @param {rustdoc.ResultsTable} results
- * @param {boolean} go_to_first
+ * @param {boolean} goToFirst
  * @param {string} filterCrates
  */
-async function showResults(results, go_to_first, filterCrates) {
-    const search = searchState.outputElement();
-    if (go_to_first || (results.others.length === 1
-        && getSettingValue("go-to-only-result") === "true")
-    ) {
-        // Needed to force re-execution of JS when coming back to a page. Let's take this
-        // scenario as example:
-        //
-        // 1. You have the "Directly go to item in search if there is only one result" option
-        //    enabled.
-        // 2. You make a search which results only one result, leading you automatically to
-        //    this result.
-        // 3. You go back to previous page.
-        //
-        // Now, without the call below, the JS will not be re-executed and the previous state
-        // will be used, starting search again since the search input is not empty, leading you
-        // back to the previous page again.
-        window.onunload = () => { };
-        searchState.removeQueryParameters();
-        const elem = document.createElement("a");
-        elem.href = results.others[0].href;
-        removeClass(elem, "active");
-        // For firefox, we need the element to be in the DOM so it can be clicked.
-        document.body.appendChild(elem);
-        elem.click();
-        return;
-    }
-    if (results.query === undefined) {
-        // @ts-expect-error
-        results.query = DocSearch.parseQuery(searchState.input.value);
-    }
+async function showResults(docSearch, results, goToFirst, filterCrates) {
+    const search = window.searchState.outputElement();
 
-    currentResults = results.query.userQuery;
-
-    // Navigate to the relevant tab if the current tab is empty, like in case users search
-    // for "-> String". If they had selected another tab previously, they have to click on
-    // it again.
-    let currentTab = searchState.currentTab;
-    if ((currentTab === 0 && results.others.length === 0) ||
-        (currentTab === 1 && results.in_args.length === 0) ||
-        (currentTab === 2 && results.returned.length === 0)) {
-        if (results.others.length !== 0) {
-            currentTab = 0;
-        } else if (results.in_args.length) {
-            currentTab = 1;
-        } else if (results.returned.length) {
-            currentTab = 2;
-        }
+    if (!search) {
+        return;
     }
 
     let crates = "";
-    if (rawSearchIndex.size > 1) {
-        crates = "<div class=\"sub-heading\"> in&nbsp;<div id=\"crate-search-div\">" +
+    const crateNames = await docSearch.getCrateNameList();
+    if (crateNames.length > 1) {
+        crates = "&nbsp;in&nbsp;<div id=\"crate-search-div\">" +
             "<select id=\"crate-search\"><option value=\"all crates\">all crates</option>";
-        for (const c of rawSearchIndex.keys()) {
+        const l = crateNames.length;
+        for (let i = 0; i < l; i += 1) {
+            const c = crateNames[i];
             crates += `<option value="${c}" ${c === filterCrates && "selected"}>${c}</option>`;
         }
-        crates += "</select></div></div>";
+        crates += "</select></div>";
     }
+    nonnull(document.querySelector(".search-switcher")).innerHTML = `Search results${crates}`;
 
-    let output = `<div class="main-heading">\
-        <h1 class="search-results-title">Results</h1>${crates}</div>`;
+    /** @type {[HTMLElement, Promise<HTMLElement>][]} */
+    const tabs = [];
+    searchState.currentTab = 0;
     if (results.query.error !== null) {
-        const error = results.query.error;
-        // @ts-expect-error
-        error.forEach((value, index) => {
-            value = value.split("<").join("&lt;").split(">").join("&gt;");
-            if (index % 2 !== 0) {
-                error[index] = `<code>${value.replaceAll(" ", "&nbsp;")}</code>`;
-            } else {
-                error[index] = value;
-            }
-        });
-        output += `<h3 class="error">Query parser error: "${error.join("")}".</h3>`;
-        output += "<div id=\"search-tabs\">" +
-            makeTabHeader(0, "In Names", results.others.length) +
-            "</div>";
-        currentTab = 0;
-    } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) {
-        output += "<div id=\"search-tabs\">" +
-            makeTabHeader(0, "In Names", results.others.length) +
-            makeTabHeader(1, "In Parameters", results.in_args.length) +
-            makeTabHeader(2, "In Return Types", results.returned.length) +
-            "</div>";
+        tabs.push(makeTab(0, "In Names", results.others, results.query, false, goToFirst));
+    } else if (
+        results.query.foundElems <= 1 &&
+        results.query.returned.length === 0 &&
+        !results.query.hasReturnArrow
+    ) {
+        tabs.push(makeTab(0, "In Names", results.others, results.query, false, goToFirst));
+        tabs.push(makeTab(1, "In Parameters", results.in_args, results.query, true, false));
+        tabs.push(makeTab(2, "In Return Types", results.returned, results.query, true, false));
     } else {
         const signatureTabTitle =
             results.query.elems.length === 0 ? "In Function Return Types" :
                 results.query.returned.length === 0 ? "In Function Parameters" :
                     "In Function Signatures";
-        output += "<div id=\"search-tabs\">" +
-            makeTabHeader(0, signatureTabTitle, results.others.length) +
-            "</div>";
-        currentTab = 0;
+        tabs.push(makeTab(0, signatureTabTitle, results.others, results.query, true, goToFirst));
     }
 
-    if (results.query.correction !== null) {
-        const orig = results.query.returned.length > 0
-            ? results.query.returned[0].name
-            : results.query.elems[0].name;
-        output += "<h3 class=\"search-corrections\">" +
-            `Type "${orig}" not found. ` +
-            "Showing results for closest type name " +
-            `"${results.query.correction}" instead.</h3>`;
-    }
-    if (results.query.proposeCorrectionFrom !== null) {
-        const orig = results.query.proposeCorrectionFrom;
-        const targ = results.query.proposeCorrectionTo;
-        output += "<h3 class=\"search-corrections\">" +
-            `Type "${orig}" not found and used as generic parameter. ` +
-            `Consider searching for "${targ}" instead.</h3>`;
-    }
-
-    const [ret_others, ret_in_args, ret_returned] = await Promise.all([
-        addTab(results.others, results.query, currentTab === 0),
-        addTab(results.in_args, results.query, currentTab === 1),
-        addTab(results.returned, results.query, currentTab === 2),
-    ]);
+    const tabsElem = document.createElement("div");
+    tabsElem.id = "search-tabs";
 
     const resultsElem = document.createElement("div");
     resultsElem.id = "results";
-    resultsElem.appendChild(ret_others);
-    resultsElem.appendChild(ret_in_args);
-    resultsElem.appendChild(ret_returned);
 
-    // @ts-expect-error
-    search.innerHTML = output;
-    if (searchState.rustdocToolbar) {
-        // @ts-expect-error
-        search.querySelector(".main-heading").appendChild(searchState.rustdocToolbar);
+    search.innerHTML = "";
+    for (const [tab, output] of tabs) {
+        tabsElem.appendChild(tab);
+        const placeholder = document.createElement("div");
+        output.then(output => {
+            if (placeholder.parentElement) {
+                placeholder.parentElement.replaceChild(output, placeholder);
+            }
+        });
+        resultsElem.appendChild(placeholder);
+    }
+
+    if (window.searchState.rustdocToolbar) {
+        nonnull(
+            nonnull(window.searchState.containerElement())
+                .querySelector(".main-heading"),
+        ).appendChild(window.searchState.rustdocToolbar);
     }
     const crateSearch = document.getElementById("crate-search");
     if (crateSearch) {
         crateSearch.addEventListener("input", updateCrate);
     }
-    // @ts-expect-error
+    search.appendChild(tabsElem);
     search.appendChild(resultsElem);
     // Reset focused elements.
-    searchState.showResults(search);
-    // @ts-expect-error
-    const elems = document.getElementById("search-tabs").childNodes;
-    // @ts-expect-error
-    searchState.focusedByTab = [];
+    window.searchState.showResults();
+    window.searchState.focusedByTab = [null, null, null];
     let i = 0;
-    for (const elem of elems) {
+    for (const elem of tabsElem.childNodes) {
         const j = i;
         // @ts-expect-error
         elem.onclick = () => printTab(j);
-        searchState.focusedByTab.push(null);
+        window.searchState.focusedByTab[i] = null;
         i += 1;
     }
-    printTab(currentTab);
+    printTab(0);
 }
 
 // @ts-expect-error
 function updateSearchHistory(url) {
+    const btn = document.querySelector("#search-button a");
+    if (btn instanceof HTMLAnchorElement) {
+        btn.href = url;
+    }
     if (!browserSupportsHistoryApi()) {
         return;
     }
     const params = searchState.getQueryStringParams();
-    if (!history.state && !params.search) {
+    if (!history.state && params.search === undefined) {
         history.pushState(null, "", url);
     } else {
         history.replaceState(null, "", url);
@@ -5152,8 +5046,8 @@ function updateSearchHistory(url) {
  * @param {boolean} [forced]
  */
 async function search(forced) {
-    // @ts-expect-error
-    const query = DocSearch.parseQuery(searchState.input.value.trim());
+    const query = DocSearch.parseQuery(nonnull(window.searchState.inputElement()).value.trim());
+
     let filterCrates = getFilterCrates();
 
     // @ts-expect-error
@@ -5163,6 +5057,7 @@ async function search(forced) {
         }
         return;
     }
+    currentResults = query.userQuery;
 
     searchState.setLoadingSearch();
 
@@ -5174,6 +5069,12 @@ async function search(forced) {
         filterCrates = params["filter-crate"];
     }
 
+    if (filterCrates !== null &&
+        (await docSearch.getCrateNameList()).indexOf(filterCrates) === -1
+    ) {
+        filterCrates = null;
+    }
+
     // Update document title to maintain a meaningful browser history
     searchState.title = "\"" + query.userQuery + "\" Search - Rust";
 
@@ -5182,6 +5083,7 @@ async function search(forced) {
     updateSearchHistory(buildUrl(query.userQuery, filterCrates));
 
     await showResults(
+        docSearch,
         // @ts-expect-error
         await docSearch.execQuery(query, filterCrates, window.currentCrate),
         params.go_to_first,
@@ -5201,16 +5103,14 @@ function onSearchSubmit(e) {
 }
 
 function putBackSearch() {
-    const search_input = searchState.input;
-    if (!searchState.input) {
+    const search_input = window.searchState.inputElement();
+    if (!search_input) {
         return;
     }
-    // @ts-expect-error
     if (search_input.value !== "" && !searchState.isDisplayed()) {
         searchState.showResults();
         if (browserSupportsHistoryApi()) {
             history.replaceState(null, "",
-                // @ts-expect-error
                 buildUrl(search_input.value, getFilterCrates()));
         }
         document.title = searchState.title;
@@ -5224,30 +5124,21 @@ function registerSearchEvents() {
     // but only if the input bar is empty. This avoid the obnoxious issue
     // where you start trying to do a search, and the index loads, and
     // suddenly your search is gone!
-    // @ts-expect-error
-    if (searchState.input.value === "") {
-        // @ts-expect-error
-        searchState.input.value = params.search || "";
+    const inputElement = nonnull(window.searchState.inputElement());
+    if (inputElement.value === "") {
+        inputElement.value = params.search || "";
     }
 
     const searchAfter500ms = () => {
         searchState.clearInputTimeout();
-        // @ts-expect-error
-        if (searchState.input.value.length === 0) {
-            searchState.hideResults();
-        } else {
-            // @ts-ignore
-            searchState.timeout = setTimeout(search, 500);
-        }
+        window.searchState.timeout = setTimeout(search, 500);
     };
-    // @ts-expect-error
-    searchState.input.onkeyup = searchAfter500ms;
-    // @ts-expect-error
-    searchState.input.oninput = searchAfter500ms;
-    // @ts-expect-error
-    document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit;
-    // @ts-expect-error
-    searchState.input.onchange = e => {
+    inputElement.onkeyup = searchAfter500ms;
+    inputElement.oninput = searchAfter500ms;
+    if (inputElement.form) {
+        inputElement.form.onsubmit = onSearchSubmit;
+    }
+    inputElement.onchange = e => {
         if (e.target !== document.activeElement) {
             // To prevent doing anything when it's from a blur event.
             return;
@@ -5259,11 +5150,13 @@ function registerSearchEvents() {
         // change, though.
         setTimeout(search, 0);
     };
-    // @ts-expect-error
-    searchState.input.onpaste = searchState.input.onchange;
+    inputElement.onpaste = inputElement.onchange;
 
     // @ts-expect-error
     searchState.outputElement().addEventListener("keydown", e => {
+        if (!(e instanceof KeyboardEvent)) {
+            return;
+        }
         // We only handle unmodified keystrokes here. We don't want to interfere with,
         // for instance, alt-left and alt-right for history navigation.
         if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
@@ -5303,88 +5196,23 @@ function registerSearchEvents() {
         }
     });
 
-    // @ts-expect-error
-    searchState.input.addEventListener("keydown", e => {
+    inputElement.addEventListener("keydown", e => {
         if (e.which === 40) { // down
             focusSearchResult();
             e.preventDefault();
         }
     });
 
-    // @ts-expect-error
-    searchState.input.addEventListener("focus", () => {
+    inputElement.addEventListener("focus", () => {
         putBackSearch();
     });
-
-    // @ts-expect-error
-    searchState.input.addEventListener("blur", () => {
-        if (window.searchState.input) {
-            window.searchState.input.placeholder = window.searchState.origPlaceholder;
-        }
-    });
-
-    // Push and pop states are used to add search results to the browser
-    // history.
-    if (browserSupportsHistoryApi()) {
-        // Store the previous <title> so we can revert back to it later.
-        const previousTitle = document.title;
-
-        window.addEventListener("popstate", e => {
-            const params = searchState.getQueryStringParams();
-            // Revert to the previous title manually since the History
-            // API ignores the title parameter.
-            document.title = previousTitle;
-            // When browsing forward to search results the previous
-            // search will be repeated, so the currentResults are
-            // cleared to ensure the search is successful.
-            currentResults = null;
-            // Synchronize search bar with query string state and
-            // perform the search. This will empty the bar if there's
-            // nothing there, which lets you really go back to a
-            // previous state with nothing in the bar.
-            if (params.search && params.search.length > 0) {
-                // @ts-expect-error
-                searchState.input.value = params.search;
-                // Some browsers fire "onpopstate" for every page load
-                // (Chrome), while others fire the event only when actually
-                // popping a state (Firefox), which is why search() is
-                // called both here and at the end of the startSearch()
-                // function.
-                e.preventDefault();
-                search();
-            } else {
-                // @ts-expect-error
-                searchState.input.value = "";
-                // When browsing back from search results the main page
-                // visibility must be reset.
-                searchState.hideResults();
-            }
-        });
-    }
-
-    // This is required in firefox to avoid this problem: Navigating to a search result
-    // with the keyboard, hitting enter, and then hitting back would take you back to
-    // the doc page, rather than the search that should overlay it.
-    // This was an interaction between the back-forward cache and our handlers
-    // that try to sync state between the URL and the search input. To work around it,
-    // do a small amount of re-init on page show.
-    window.onpageshow = () => {
-        const qSearch = searchState.getQueryStringParams().search;
-        // @ts-expect-error
-        if (searchState.input.value === "" && qSearch) {
-            // @ts-expect-error
-            searchState.input.value = qSearch;
-        }
-        search();
-    };
 }
 
 // @ts-expect-error
 function updateCrate(ev) {
     if (ev.target.value === "all crates") {
         // If we don't remove it from the URL, it'll be picked up again by the search.
-        // @ts-expect-error
-        const query = searchState.input.value.trim();
+        const query = nonnull(window.searchState.inputElement()).value.trim();
         updateSearchHistory(buildUrl(query, null));
     }
     // In case you "cut" the entry from the search input, then change the crate filter
@@ -5394,522 +5222,89 @@ function updateCrate(ev) {
     search(true);
 }
 
-// Parts of this code are based on Lucene, which is licensed under the
-// Apache/2.0 license.
-// More information found here:
-// https://fossies.org/linux/lucene/lucene/core/src/java/org/apache/lucene/util/automaton/
-//   LevenshteinAutomata.java
-class ParametricDescription {
-    // @ts-expect-error
-    constructor(w, n, minErrors) {
-        this.w = w;
-        this.n = n;
-        this.minErrors = minErrors;
-    }
-    // @ts-expect-error
-    isAccept(absState) {
-        const state = Math.floor(absState / (this.w + 1));
-        const offset = absState % (this.w + 1);
-        return this.w - offset + this.minErrors[state] <= this.n;
-    }
-    // @ts-expect-error
-    getPosition(absState) {
-        return absState % (this.w + 1);
-    }
-    // @ts-expect-error
-    getVector(name, charCode, pos, end) {
-        let vector = 0;
-        for (let i = pos; i < end; i += 1) {
-            vector = vector << 1;
-            if (name.charCodeAt(i) === charCode) {
-                vector |= 1;
-            }
-        }
-        return vector;
-    }
-    // @ts-expect-error
-    unpack(data, index, bitsPerValue) {
-        const bitLoc = (bitsPerValue * index);
-        const dataLoc = bitLoc >> 5;
-        const bitStart = bitLoc & 31;
-        if (bitStart + bitsPerValue <= 32) {
-            // not split
-            return ((data[dataLoc] >> bitStart) & this.MASKS[bitsPerValue - 1]);
-        } else {
-            // split
-            const part = 32 - bitStart;
-            return ~~(((data[dataLoc] >> bitStart) & this.MASKS[part - 1]) +
-                ((data[1 + dataLoc] & this.MASKS[bitsPerValue - part - 1]) << part));
-        }
+// eslint-disable-next-line max-len
+// polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64
+/**
+ * @type {function(string): Uint8Array} base64
+ */
+//@ts-expect-error
+const makeUint8ArrayFromBase64 = Uint8Array.fromBase64 ? Uint8Array.fromBase64 : (string => {
+    const bytes_as_string = atob(string);
+    const l = bytes_as_string.length;
+    const bytes = new Uint8Array(l);
+    for (let i = 0; i < l; ++i) {
+        bytes[i] = bytes_as_string.charCodeAt(i);
     }
-}
-ParametricDescription.prototype.MASKS = new Int32Array([
-    0x1, 0x3, 0x7, 0xF,
-    0x1F, 0x3F, 0x7F, 0xFF,
-    0x1FF, 0x3F, 0x7FF, 0xFFF,
-    0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF,
-    0x1FFFF, 0x3FFFF, 0x7FFFF, 0xFFFFF,
-    0x1FFFFF, 0x3FFFFF, 0x7FFFFF, 0xFFFFFF,
-    0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF,
-    0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF,
-]);
-
-// The following code was generated with the moman/finenight pkg
-// This package is available under the MIT License, see NOTICE.txt
-// for more details.
-// This class is auto-generated, Please do not modify it directly.
-// You should modify the https://gitlab.com/notriddle/createAutomata.py instead.
-// The following code was generated with the moman/finenight pkg
-// This package is available under the MIT License, see NOTICE.txt
-// for more details.
-// This class is auto-generated, Please do not modify it directly.
-// You should modify https://gitlab.com/notriddle/moman-rustdoc instead.
-
-class Lev2TParametricDescription extends ParametricDescription {
-    /**
-     * @param {number} absState
-     * @param {number} position
-     * @param {number} vector
-     * @returns {number}
-    */
-    transition(absState, position, vector) {
-        let state = Math.floor(absState / (this.w + 1));
-        let offset = absState % (this.w + 1);
-
-        if (position === this.w) {
-            if (state < 3) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 3) + state;
-                offset += this.unpack(this.offsetIncrs0, loc, 1);
-                state = this.unpack(this.toStates0, loc, 2) - 1;
-            }
-        } else if (position === this.w - 1) {
-            if (state < 5) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 5) + state;
-                offset += this.unpack(this.offsetIncrs1, loc, 1);
-                state = this.unpack(this.toStates1, loc, 3) - 1;
-            }
-        } else if (position === this.w - 2) {
-            if (state < 13) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 13) + state;
-                offset += this.unpack(this.offsetIncrs2, loc, 2);
-                state = this.unpack(this.toStates2, loc, 4) - 1;
-            }
-        } else if (position === this.w - 3) {
-            if (state < 28) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 28) + state;
-                offset += this.unpack(this.offsetIncrs3, loc, 2);
-                state = this.unpack(this.toStates3, loc, 5) - 1;
-            }
-        } else if (position === this.w - 4) {
-            if (state < 45) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 45) + state;
-                offset += this.unpack(this.offsetIncrs4, loc, 3);
-                state = this.unpack(this.toStates4, loc, 6) - 1;
-            }
-        } else {
-            if (state < 45) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 45) + state;
-                offset += this.unpack(this.offsetIncrs5, loc, 3);
-                state = this.unpack(this.toStates5, loc, 6) - 1;
-            }
-        }
+    return bytes;
+});
 
-        if (state === -1) {
-            // null state
-            return -1;
-        } else {
-            // translate back to abs
-            return Math.imul(state, this.w + 1) + offset;
-        }
-    }
 
-    // state map
-    //   0 -> [(0, 0)]
-    //   1 -> [(0, 1)]
-    //   2 -> [(0, 2)]
-    //   3 -> [(0, 1), (1, 1)]
-    //   4 -> [(0, 2), (1, 2)]
-    //   5 -> [(0, 1), (1, 1), (2, 1)]
-    //   6 -> [(0, 2), (1, 2), (2, 2)]
-    //   7 -> [(0, 1), (2, 1)]
-    //   8 -> [(0, 1), (2, 2)]
-    //   9 -> [(0, 2), (2, 1)]
-    //   10 -> [(0, 2), (2, 2)]
-    //   11 -> [t(0, 1), (0, 1), (1, 1), (2, 1)]
-    //   12 -> [t(0, 2), (0, 2), (1, 2), (2, 2)]
-    //   13 -> [(0, 2), (1, 2), (2, 2), (3, 2)]
-    //   14 -> [(0, 1), (1, 1), (3, 2)]
-    //   15 -> [(0, 1), (2, 2), (3, 2)]
-    //   16 -> [(0, 1), (3, 2)]
-    //   17 -> [(0, 1), t(1, 2), (2, 2), (3, 2)]
-    //   18 -> [(0, 2), (1, 2), (3, 1)]
-    //   19 -> [(0, 2), (1, 2), (3, 2)]
-    //   20 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2)]
-    //   21 -> [(0, 2), (2, 1), (3, 1)]
-    //   22 -> [(0, 2), (2, 2), (3, 2)]
-    //   23 -> [(0, 2), (3, 1)]
-    //   24 -> [(0, 2), (3, 2)]
-    //   25 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2)]
-    //   26 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2)]
-    //   27 -> [t(0, 2), (0, 2), (1, 2), (3, 1)]
-    //   28 -> [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
-    //   29 -> [(0, 2), (1, 2), (2, 2), (4, 2)]
-    //   30 -> [(0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)]
-    //   31 -> [(0, 2), (1, 2), (3, 2), (4, 2)]
-    //   32 -> [(0, 2), (1, 2), (4, 2)]
-    //   33 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2), (4, 2)]
-    //   34 -> [(0, 2), (1, 2), t(2, 2), (2, 2), (3, 2), (4, 2)]
-    //   35 -> [(0, 2), (2, 1), (4, 2)]
-    //   36 -> [(0, 2), (2, 2), (3, 2), (4, 2)]
-    //   37 -> [(0, 2), (2, 2), (4, 2)]
-    //   38 -> [(0, 2), (3, 2), (4, 2)]
-    //   39 -> [(0, 2), (4, 2)]
-    //   40 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
-    //   41 -> [(0, 2), t(2, 2), (2, 2), (3, 2), (4, 2)]
-    //   42 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
-    //   43 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (4, 2)]
-    //   44 -> [t(0, 2), (0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)]
-
-
-    /** @param {number} w - length of word being checked */
-    constructor(w) {
-        super(w, 2, new Int32Array([
-            0,1,2,0,1,-1,0,-1,0,-1,0,-1,0,-1,-1,-1,-1,-1,-2,-1,-1,-2,-1,-2,
-            -1,-1,-1,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,
-        ]));
+if (ROOT_PATH === null) {
+    return;
+}
+const database = await Stringdex.loadDatabase(hooks);
+if (typeof window !== "undefined") {
+    docSearch = new DocSearch(ROOT_PATH, database);
+    await docSearch.buildIndex();
+    onEachLazy(document.querySelectorAll(
+        ".search-form.loading",
+    ), form => {
+        removeClass(form, "loading");
+    });
+    registerSearchEvents();
+    // If there's a search term in the URL, execute the search now.
+    if (window.searchState.getQueryStringParams().search !== undefined) {
+        search();
     }
+} else if (typeof exports !== "undefined") {
+    docSearch = new DocSearch(ROOT_PATH, database);
+    await docSearch.buildIndex();
+    return { docSearch, DocSearch };
 }
+};
 
-Lev2TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([
-    0xe,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([
-    0x0,
-]);
-
-Lev2TParametricDescription.prototype.toStates1 = /*3 bits per value */ new Int32Array([
-    0x1a688a2c,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([
-    0x3e0,
-]);
-
-Lev2TParametricDescription.prototype.toStates2 = /*4 bits per value */ new Int32Array([
-    0x70707054,0xdc07035,0x3dd3a3a,0x2323213a,
-    0x15435223,0x22545432,0x5435,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([
-    0x80000,0x55582088,0x55555555,0x55,
-]);
-
-Lev2TParametricDescription.prototype.toStates3 = /*5 bits per value */ new Int32Array([
-    0x1c0380a4,0x700a570,0xca529c0,0x180a00,
-    0xa80af180,0xc5498e60,0x5a546398,0x8c4300e8,
-    0xac18c601,0xd8d43501,0x863500ad,0x51976d6a,
-    0x8ca0180a,0xc3501ac2,0xb0c5be16,0x76dda8a5,
-    0x18c4519,0xc41294a,0xe248d231,0x1086520c,
-    0xce31ac42,0x13946358,0x2d0348c4,0x6732d494,
-    0x1ad224a5,0xd635ad4b,0x520c4139,0xce24948,
-    0x22110a52,0x58ce729d,0xc41394e3,0x941cc520,
-    0x90e732d4,0x4729d224,0x39ce35ad,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([
-    0x80000,0xc0c830,0x300f3c30,0x2200fcff,
-    0xcaa00a08,0x3c2200a8,0xa8fea00a,0x55555555,
-    0x55555555,0x55555555,0x55555555,0x55555555,
-    0x55555555,0x55555555,
-]);
-
-Lev2TParametricDescription.prototype.toStates4 = /*6 bits per value */ new Int32Array([
-    0x801c0144,0x1453803,0x14700038,0xc0005145,
-    0x1401,0x14,0x140000,0x0,
-    0x510000,0x6301f007,0x301f00d1,0xa186178,
-    0xc20ca0c3,0xc20c30,0xc30030c,0xc00c00cd,
-    0xf0c00c30,0x4c054014,0xc30944c3,0x55150c34,
-    0x8300550,0x430c0143,0x50c31,0xc30850c,
-    0xc3143000,0x50053c50,0x5130d301,0x850d30c2,
-    0x30a08608,0xc214414,0x43142145,0x21450031,
-    0x1400c314,0x4c143145,0x32832803,0x28014d6c,
-    0xcd34a0c3,0x1c50c76,0x1c314014,0x430c30c3,
-    0x1431,0xc300500,0xca00d303,0xd36d0e40,
-    0x90b0e400,0xcb2abb2c,0x70c20ca1,0x2c32ca2c,
-    0xcd2c70cb,0x31c00c00,0x34c2c32c,0x5583280,
-    0x558309b7,0x6cd6ca14,0x430850c7,0x51c51401,
-    0x1430c714,0xc3087,0x71451450,0xca00d30,
-    0xc26dc156,0xb9071560,0x1cb2abb2,0xc70c2144,
-    0xb1c51ca1,0x1421c70c,0xc51c00c3,0x30811c51,
-    0x24324308,0xc51031c2,0x70820820,0x5c33830d,
-    0xc33850c3,0x30c30c30,0xc30c31c,0x451450c3,
-    0x20c20c20,0xda0920d,0x5145914f,0x36596114,
-    0x51965865,0xd9643653,0x365a6590,0x51964364,
-    0x43081505,0x920b2032,0x2c718b28,0xd7242249,
-    0x35cb28b0,0x2cb3872c,0x972c30d7,0xb0c32cb2,
-    0x4e1c75c,0xc80c90c2,0x62ca2482,0x4504171c,
-    0xd65d9610,0x33976585,0xd95cb5d,0x4b5ca5d7,
-    0x73975c36,0x10308138,0xc2245105,0x41451031,
-    0x14e24208,0xc35c3387,0x51453851,0x1c51c514,
-    0xc70c30c3,0x20451450,0x14f1440c,0x4f0da092,
-    0x4513d41,0x6533944d,0x1350e658,0xe1545055,
-    0x64365a50,0x5519383,0x51030815,0x28920718,
-    0x441c718b,0x714e2422,0x1c35cb28,0x4e1c7387,
-    0xb28e1c51,0x5c70c32c,0xc204e1c7,0x81c61440,
-    0x1c62ca24,0xd04503ce,0x85d63944,0x39338e65,
-    0x8e154387,0x364b5ca3,0x38739738,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs4 = /*3 bits per value */ new Int32Array([
-    0x10000000,0xc00000,0x60061,0x400,
-    0x0,0x80010008,0x249248a4,0x8229048,
-    0x2092,0x6c3603,0xb61b6c30,0x6db6036d,
-    0xdb6c0,0x361b0180,0x91b72000,0xdb11b71b,
-    0x6db6236,0x1008200,0x12480012,0x24924906,
-    0x48200049,0x80410002,0x24000900,0x4924a489,
-    0x10822492,0x20800125,0x48360,0x9241b692,
-    0x6da4924,0x40009268,0x241b010,0x291b4900,
-    0x6d249249,0x49493423,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x2492,
-]);
-
-Lev2TParametricDescription.prototype.toStates5 = /*6 bits per value */ new Int32Array([
-    0x801c0144,0x1453803,0x14700038,0xc0005145,
-    0x1401,0x14,0x140000,0x0,
-    0x510000,0x4e00e007,0xe0051,0x3451451c,
-    0xd015000,0x30cd0000,0xc30c30c,0xc30c30d4,
-    0x40c30c30,0x7c01c014,0xc03458c0,0x185e0c07,
-    0x2830c286,0x830c3083,0xc30030,0x33430c,
-    0x30c3003,0x70051030,0x16301f00,0x8301f00d,
-    0x30a18617,0xc20ca0c,0x431420c3,0xb1450c51,
-    0x14314315,0x4f143145,0x34c05401,0x4c30944c,
-    0x55150c3,0x30830055,0x1430c014,0xc00050c3,
-    0xc30850,0xc314300,0x150053c5,0x25130d30,
-    0x5430d30c,0xc0354154,0x300d0c90,0x1cb2cd0c,
-    0xc91cb0c3,0x72c30cb2,0x14f1cb2c,0xc34c0540,
-    0x34c30944,0x82182214,0x851050c2,0x50851430,
-    0x1400c50c,0x30c5085,0x50c51450,0x150053c,
-    0xc25130d3,0x8850d30,0x1430a086,0x450c2144,
-    0x51cb1c21,0x1c91c70c,0xc71c314b,0x34c1cb1,
-    0x6c328328,0xc328014d,0x76cd34a0,0x1401c50c,
-    0xc31c3140,0x31430c30,0x14,0x30c3005,
-    0xa0ca00d3,0x535b0c,0x4d2830ca,0x514369b3,
-    0xc500d01,0x5965965a,0x30d46546,0x6435030c,
-    0x8034c659,0xdb439032,0x2c390034,0xcaaecb24,
-    0x30832872,0xcb28b1c,0x4b1c32cb,0x70030033,
-    0x30b0cb0c,0xe40ca00d,0x400d36d0,0xb2c90b0e,
-    0xca1cb2ab,0xa2c70c20,0x6575d95c,0x4315b5ce,
-    0x95c53831,0x28034c5d,0x9b705583,0xa1455830,
-    0xc76cd6c,0x40143085,0x71451c51,0x871430c,
-    0x450000c3,0xd3071451,0x1560ca00,0x560c26dc,
-    0xb35b2851,0xc914369,0x1a14500d,0x46593945,
-    0xcb2c939,0x94507503,0x328034c3,0x9b70558,
-    0xe41c5583,0x72caaeca,0x1c308510,0xc7147287,
-    0x50871c32,0x1470030c,0xd307147,0xc1560ca0,
-    0x1560c26d,0xabb2b907,0x21441cb2,0x38a1c70c,
-    0x8e657394,0x314b1c93,0x39438738,0x43083081,
-    0x31c22432,0x820c510,0x830d7082,0x50c35c33,
-    0xc30c338,0xc31c30c3,0x50c30c30,0xc204514,
-    0x890c90c2,0x31440c70,0xa8208208,0xea0df0c3,
-    0x8a231430,0xa28a28a2,0x28a28a1e,0x1861868a,
-    0x48308308,0xc3682483,0x14516453,0x4d965845,
-    0xd4659619,0x36590d94,0xd969964,0x546590d9,
-    0x20c20541,0x920d20c,0x5914f0da,0x96114514,
-    0x65865365,0xe89d3519,0x99e7a279,0x9e89e89e,
-    0x81821827,0xb2032430,0x18b28920,0x422492c7,
-    0xb28b0d72,0x3872c35c,0xc30d72cb,0x32cb2972,
-    0x1c75cb0c,0xc90c204e,0xa2482c80,0x24b1c62c,
-    0xc3a89089,0xb0ea2e42,0x9669a31c,0xa4966a28,
-    0x59a8a269,0x8175e7a,0xb203243,0x718b2892,
-    0x4114105c,0x17597658,0x74ce5d96,0x5c36572d,
-    0xd92d7297,0xe1ce5d70,0xc90c204,0xca2482c8,
-    0x4171c62,0x5d961045,0x976585d6,0x79669533,
-    0x964965a2,0x659689e6,0x308175e7,0x24510510,
-    0x451031c2,0xe2420841,0x5c338714,0x453851c3,
-    0x51c51451,0xc30c31c,0x451450c7,0x41440c20,
-    0xc708914,0x82105144,0xf1c58c90,0x1470ea0d,
-    0x61861863,0x8a1e85e8,0x8687a8a2,0x3081861,
-    0x24853c51,0x5053c368,0x1341144f,0x96194ce5,
-    0x1544d439,0x94385514,0xe0d90d96,0x5415464,
-    0x4f1440c2,0xf0da0921,0x4513d414,0x533944d0,
-    0x350e6586,0x86082181,0xe89e981d,0x18277689,
-    0x10308182,0x89207185,0x41c718b2,0x14e24224,
-    0xc35cb287,0xe1c73871,0x28e1c514,0xc70c32cb,
-    0x204e1c75,0x1c61440c,0xc62ca248,0x90891071,
-    0x2e41c58c,0xa31c70ea,0xe86175e7,0xa269a475,
-    0x5e7a57a8,0x51030817,0x28920718,0xf38718b,
-    0xe5134114,0x39961758,0xe1ce4ce,0x728e3855,
-    0x5ce0d92d,0xc204e1ce,0x81c61440,0x1c62ca24,
-    0xd04503ce,0x85d63944,0x75338e65,0x5d86075e,
-    0x89e69647,0x75e76576,
-]);
-Lev2TParametricDescription.prototype.offsetIncrs5 = /*3 bits per value */ new Int32Array([
-    0x10000000,0xc00000,0x60061,0x400,
-    0x0,0x60000008,0x6b003080,0xdb6ab6db,
-    0x2db6,0x800400,0x49245240,0x11482412,
-    0x104904,0x40020000,0x92292000,0xa4b25924,
-    0x9649658,0xd80c000,0xdb0c001b,0x80db6d86,
-    0x6db01b6d,0xc0600003,0x86000d86,0x6db6c36d,
-    0xddadb6ed,0x300001b6,0x6c360,0xe37236e4,
-    0x46db6236,0xdb6c,0x361b018,0xb91b7200,
-    0x6dbb1b71,0x6db763,0x20100820,0x61248001,
-    0x92492490,0x24820004,0x8041000,0x92400090,
-    0x24924830,0x555b6a49,0x2080012,0x20004804,
-    0x49252449,0x84112492,0x4000928,0x240201,
-    0x92922490,0x58924924,0x49456,0x120d8082,
-    0x6da4800,0x69249249,0x249a01b,0x6c04100,
-    0x6d240009,0x92492483,0x24d5adb4,0x60208001,
-    0x92000483,0x24925236,0x6846da49,0x10400092,
-    0x241b0,0x49291b49,0x636d2492,0x92494935,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,0x49249249,
-    0x92492492,0x24924924,0x49249249,0x92492492,
-    0x24924924,0x49249249,0x92492492,0x24924924,
-    0x49249249,0x92492492,0x24924924,
-]);
-
-class Lev1TParametricDescription extends ParametricDescription {
-    /**
-     * @param {number} absState
-     * @param {number} position
-     * @param {number} vector
-     * @returns {number}
-    */
-    transition(absState, position, vector) {
-        let state = Math.floor(absState / (this.w + 1));
-        let offset = absState % (this.w + 1);
-
-        if (position === this.w) {
-            if (state < 2) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 2) + state;
-                offset += this.unpack(this.offsetIncrs0, loc, 1);
-                state = this.unpack(this.toStates0, loc, 2) - 1;
-            }
-        } else if (position === this.w - 1) {
-            if (state < 3) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 3) + state;
-                offset += this.unpack(this.offsetIncrs1, loc, 1);
-                state = this.unpack(this.toStates1, loc, 2) - 1;
+if (typeof window !== "undefined") {
+    const ROOT_PATH = window.rootPath;
+    /** @type {stringdex.Callbacks|null} */
+    let databaseCallbacks = null;
+    initSearch(window.Stringdex, window.RoaringBitmap, {
+        loadRoot: callbacks => {
+            for (const key in callbacks) {
+                if (Object.hasOwn(callbacks, key)) {
+                    // @ts-ignore
+                    window[key] = callbacks[key];
+                }
             }
-        } else if (position === this.w - 2) {
-            if (state < 6) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 6) + state;
-                offset += this.unpack(this.offsetIncrs2, loc, 2);
-                state = this.unpack(this.toStates2, loc, 3) - 1;
+            databaseCallbacks = callbacks;
+            // search.index/root is loaded by main.js, so
+            // this script doesn't need to launch it, but
+            // must pick it up
+            if (window.searchIndex) {
+                window.rr_(window.searchIndex);
             }
-        } else {
-            if (state < 6) { // eslint-disable-line no-lonely-if
-                const loc = Math.imul(vector, 6) + state;
-                offset += this.unpack(this.offsetIncrs3, loc, 2);
-                state = this.unpack(this.toStates3, loc, 3) - 1;
-            }
-        }
-
-        if (state === -1) {
-            // null state
-            return -1;
-        } else {
-            // translate back to abs
-            return Math.imul(state, this.w + 1) + offset;
-        }
-    }
-
-    // state map
-    //   0 -> [(0, 0)]
-    //   1 -> [(0, 1)]
-    //   2 -> [(0, 1), (1, 1)]
-    //   3 -> [(0, 1), (1, 1), (2, 1)]
-    //   4 -> [(0, 1), (2, 1)]
-    //   5 -> [t(0, 1), (0, 1), (1, 1), (2, 1)]
-
-
-    /** @param {number} w - length of word being checked */
-    constructor(w) {
-        super(w, 1, new Int32Array([0,1,0,-1,-1,-1]));
-    }
-}
-
-Lev1TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([
-    0x2,
-]);
-Lev1TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([
-    0x0,
-]);
-
-Lev1TParametricDescription.prototype.toStates1 = /*2 bits per value */ new Int32Array([
-    0xa43,
-]);
-Lev1TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([
-    0x38,
-]);
-
-Lev1TParametricDescription.prototype.toStates2 = /*3 bits per value */ new Int32Array([
-    0x12180003,0xb45a4914,0x69,
-]);
-Lev1TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([
-    0x558a0000,0x5555,
-]);
-
-Lev1TParametricDescription.prototype.toStates3 = /*3 bits per value */ new Int32Array([
-    0x900c0003,0xa1904864,0x45a49169,0x5a6d196a,
-    0x9634,
-]);
-Lev1TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([
-    0xa0fc0000,0x5555ba08,0x55555555,
-]);
-
-// ====================
-// WARNING: Nothing should be added below this comment: we need the `initSearch` function to
-// be called ONLY when the whole file has been parsed and loaded.
-
-// @ts-expect-error
-function initSearch(searchIndex) {
-    rawSearchIndex = searchIndex;
-    if (typeof window !== "undefined") {
-        // @ts-expect-error
-        docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState);
-        registerSearchEvents();
-        // If there's a search term in the URL, execute the search now.
-        if (window.searchState.getQueryStringParams().search) {
-            search();
-        }
-    } else if (typeof exports !== "undefined") {
-        // @ts-expect-error
-        docSearch = new DocSearch(rawSearchIndex, ROOT_PATH, searchState);
-        exports.docSearch = docSearch;
-        exports.parseQuery = DocSearch.parseQuery;
-    }
-}
-
-if (typeof exports !== "undefined") {
+        },
+        loadTreeByHash: hashHex => {
+            const script = document.createElement("script");
+            script.src = `${ROOT_PATH}search.index/${hashHex}.js`;
+            script.onerror = e => {
+                if (databaseCallbacks) {
+                    databaseCallbacks.err_rn_(hashHex, e);
+                }
+            };
+            document.documentElement.appendChild(script);
+        },
+        loadDataByNameAndHash: (name, hashHex) => {
+            const script = document.createElement("script");
+            script.src = `${ROOT_PATH}search.index/${name}/${hashHex}.js`;
+            script.onerror = e => {
+                if (databaseCallbacks) {
+                    databaseCallbacks.err_rd_(hashHex, e);
+                }
+            };
+            document.documentElement.appendChild(script);
+        },
+    });
+} else if (typeof exports !== "undefined") {
+    // eslint-disable-next-line no-undef
     exports.initSearch = initSearch;
 }
-
-if (typeof window !== "undefined") {
-    // @ts-expect-error
-    window.initSearch = initSearch;
-    // @ts-expect-error
-    if (window.searchIndex !== undefined) {
-        // @ts-expect-error
-        initSearch(window.searchIndex);
-    }
-} else {
-    // Running in Node, not a browser. Run initSearch just to produce the
-    // exports.
-    initSearch(new Map());
-}
diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js
index 2430b5829b2..347d3d0750e 100644
--- a/src/librustdoc/html/static/js/settings.js
+++ b/src/librustdoc/html/static/js/settings.js
@@ -1,7 +1,7 @@
 // Local js definitions:
 /* global getSettingValue, updateLocalStorage, updateTheme */
 /* global addClass, removeClass, onEach, onEachLazy */
-/* global MAIN_ID, getVar, getSettingsButton, getHelpButton, nonnull */
+/* global MAIN_ID, getVar, nonnull */
 
 "use strict";
 
@@ -9,18 +9,6 @@
     const isSettingsPage = window.location.pathname.endsWith("/settings.html");
 
     /**
-     * @param {Element} elem
-     * @param {EventTarget|null} target
-     */
-    function elemContainsTarget(elem, target) {
-        if (target instanceof Node) {
-            return elem.contains(target);
-        } else {
-            return false;
-        }
-    }
-
-    /**
      * @overload {"theme"|"preferred-dark-theme"|"preferred-light-theme"}
      * @param {string} settingName
      * @param {string} value
@@ -305,10 +293,12 @@
             }
         } else {
             el.setAttribute("tabindex", "-1");
-            const settingsBtn = getSettingsButton();
-            if (settingsBtn !== null) {
-                settingsBtn.appendChild(el);
-            }
+            onEachLazy(document.querySelectorAll(".settings-menu"), menu => {
+                if (menu.offsetWidth !== 0) {
+                    menu.appendChild(el);
+                    return true;
+                }
+            });
         }
         return el;
     }
@@ -317,6 +307,15 @@
 
     function displaySettings() {
         settingsMenu.style.display = "";
+        onEachLazy(document.querySelectorAll(".settings-menu"), menu => {
+            if (menu.offsetWidth !== 0) {
+                if (!menu.contains(settingsMenu) && settingsMenu.parentElement) {
+                    settingsMenu.parentElement.removeChild(settingsMenu);
+                    menu.appendChild(settingsMenu);
+                }
+                return true;
+            }
+        });
         onEachLazy(settingsMenu.querySelectorAll("input[type='checkbox']"), el => {
             const val = getSettingValue(el.id);
             const checked = val === "true";
@@ -330,40 +329,37 @@
      * @param {FocusEvent} event
      */
     function settingsBlurHandler(event) {
-        const helpBtn = getHelpButton();
-        const settingsBtn = getSettingsButton();
-        const helpUnfocused = helpBtn === null ||
-              (!helpBtn.contains(document.activeElement) &&
-               !elemContainsTarget(helpBtn, event.relatedTarget));
-        const settingsUnfocused = settingsBtn === null ||
-              (!settingsBtn.contains(document.activeElement) &&
-               !elemContainsTarget(settingsBtn, event.relatedTarget));
-        if (helpUnfocused && settingsUnfocused) {
+        const isInPopover = onEachLazy(
+            document.querySelectorAll(".settings-menu, .help-menu"),
+            menu => {
+                return menu.contains(document.activeElement) || menu.contains(event.relatedTarget);
+            },
+        );
+        if (!isInPopover) {
             window.hidePopoverMenus();
         }
     }
 
     if (!isSettingsPage) {
         // We replace the existing "onclick" callback.
-        // These elements must exist, as (outside of the settings page)
-        // `settings.js` is only loaded after the settings button is clicked.
-        const settingsButton = nonnull(getSettingsButton());
         const settingsMenu = nonnull(document.getElementById("settings"));
-        settingsButton.onclick = event => {
-            if (elemContainsTarget(settingsMenu, event.target)) {
-                return;
-            }
-            event.preventDefault();
-            const shouldDisplaySettings = settingsMenu.style.display === "none";
+        onEachLazy(document.querySelectorAll(".settings-menu"), settingsButton => {
+            /** @param {MouseEvent} event */
+            settingsButton.querySelector("a").onclick = event => {
+                if (!(event.target instanceof Element) || settingsMenu.contains(event.target)) {
+                    return;
+                }
+                event.preventDefault();
+                const shouldDisplaySettings = settingsMenu.style.display === "none";
 
-            window.hideAllModals(false);
-            if (shouldDisplaySettings) {
-                displaySettings();
-            }
-        };
-        settingsButton.onblur = settingsBlurHandler;
-        // the settings button should always have a link in it
-        nonnull(settingsButton.querySelector("a")).onblur = settingsBlurHandler;
+                window.hideAllModals(false);
+                if (shouldDisplaySettings) {
+                    displaySettings();
+                }
+            };
+            settingsButton.onblur = settingsBlurHandler;
+            settingsButton.querySelector("a").onblur = settingsBlurHandler;
+        });
         onEachLazy(settingsMenu.querySelectorAll("input"), el => {
             el.onblur = settingsBlurHandler;
         });
@@ -377,6 +373,8 @@
         if (!isSettingsPage) {
             displaySettings();
         }
-        removeClass(getSettingsButton(), "rotate");
+        onEachLazy(document.querySelectorAll(".settings-menu"), settingsButton => {
+            removeClass(settingsButton, "rotate");
+        });
     }, 0);
 })();
diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js
index ca13b891638..40ab8be03c9 100644
--- a/src/librustdoc/html/static/js/storage.js
+++ b/src/librustdoc/html/static/js/storage.js
@@ -7,6 +7,7 @@
 
 /**
  * @import * as rustdoc from "./rustdoc.d.ts";
+ * @import * as stringdex from "./stringdex.d.ts";
  */
 
 const builtinThemes = ["light", "dark", "ayu"];
@@ -116,7 +117,7 @@ function addClass(elem, className) {
  * Remove a class from a DOM Element. If `elem` is null,
  * does nothing. This function is idempotent.
  *
- * @param {Element|null} elem
+ * @param {Element|null|undefined} elem
  * @param {string} className
  */
 // eslint-disable-next-line no-unused-vars
@@ -172,7 +173,7 @@ function updateLocalStorage(name, value) {
         } else {
             window.localStorage.setItem("rustdoc-" + name, value);
         }
-    } catch (e) {
+    } catch {
         // localStorage is not accessible, do nothing
     }
 }
@@ -189,7 +190,7 @@ function updateLocalStorage(name, value) {
 function getCurrentValue(name) {
     try {
         return window.localStorage.getItem("rustdoc-" + name);
-    } catch (e) {
+    } catch {
         return null;
     }
 }
@@ -375,32 +376,6 @@ window.addEventListener("pageshow", ev => {
 // That's also why this is in storage.js and not main.js.
 //
 // [parser]: https://html.spec.whatwg.org/multipage/parsing.html
-class RustdocSearchElement extends HTMLElement {
-    constructor() {
-        super();
-    }
-    connectedCallback() {
-        const rootPath = getVar("root-path");
-        const currentCrate = getVar("current-crate");
-        this.innerHTML = `<nav class="sub">
-            <form class="search-form">
-                <span></span> <!-- This empty span is a hacky fix for Safari - See #93184 -->
-                <div id="sidebar-button" tabindex="-1">
-                    <a href="${rootPath}${currentCrate}/all.html" title="show sidebar"></a>
-                </div>
-                <input
-                    class="search-input"
-                    name="search"
-                    aria-label="Run search in the documentation"
-                    autocomplete="off"
-                    spellcheck="false"
-                    placeholder="Type ‘S’ or ‘/’ to search, ‘?’ for more options…"
-                    type="search">
-            </form>
-        </nav>`;
-    }
-}
-window.customElements.define("rustdoc-search", RustdocSearchElement);
 class RustdocToolbarElement extends HTMLElement {
     constructor() {
         super();
@@ -411,11 +386,15 @@ class RustdocToolbarElement extends HTMLElement {
             return;
         }
         const rootPath = getVar("root-path");
+        const currentUrl = window.location.href.split("?")[0].split("#")[0];
         this.innerHTML = `
-        <div id="settings-menu" tabindex="-1">
+        <div id="search-button" tabindex="-1">
+            <a href="${currentUrl}?search="><span class="label">Search</span></a>
+        </div>
+        <div class="settings-menu" tabindex="-1">
             <a href="${rootPath}settings.html"><span class="label">Settings</span></a>
         </div>
-        <div id="help-button" tabindex="-1">
+        <div class="help-menu" tabindex="-1">
             <a href="${rootPath}help.html"><span class="label">Help</span></a>
         </div>
         <button id="toggle-all-docs"
@@ -424,3 +403,31 @@ class="label">Summary</span></button>`;
     }
 }
 window.customElements.define("rustdoc-toolbar", RustdocToolbarElement);
+class RustdocTopBarElement extends HTMLElement {
+    constructor() {
+        super();
+    }
+    connectedCallback() {
+        const rootPath = getVar("root-path");
+        const tmplt = document.createElement("template");
+        tmplt.innerHTML = `
+        <slot name="sidebar-menu-toggle"></slot>
+        <slot></slot>
+        <slot name="settings-menu"></slot>
+        <slot name="help-menu"></slot>
+        `;
+        const shadow = this.attachShadow({ mode: "open" });
+        shadow.appendChild(tmplt.content.cloneNode(true));
+        this.innerHTML += `
+        <button class="sidebar-menu-toggle" slot="sidebar-menu-toggle" title="show sidebar">
+        </button>
+        <div class="settings-menu" slot="settings-menu" tabindex="-1">
+            <a href="${rootPath}settings.html"><span class="label">Settings</span></a>
+        </div>
+        <div class="help-menu" slot="help-menu" tabindex="-1">
+            <a href="${rootPath}help.html"><span class="label">Help</span></a>
+        </div>
+        `;
+    }
+}
+window.customElements.define("rustdoc-topbar", RustdocTopBarElement);
diff --git a/src/librustdoc/html/static/js/stringdex.d.ts b/src/librustdoc/html/static/js/stringdex.d.ts
new file mode 100644
index 00000000000..cf9a8b6b564
--- /dev/null
+++ b/src/librustdoc/html/static/js/stringdex.d.ts
@@ -0,0 +1,165 @@
+export = stringdex;
+
+declare namespace stringdex {
+    /**
+     * The client interface to Stringdex.
+     */
+    interface Database {
+        getIndex(colname: string): SearchTree|undefined;
+        getData(colname: string): DataColumn|undefined;
+    }
+    /**
+     * A search index file.
+     */
+    interface SearchTree {
+        trie(): Trie;
+        search(name: Uint8Array|string): Promise<Trie?>;
+        searchLev(name: Uint8Array|string): AsyncGenerator<Trie>;
+    }
+    /**
+     * A compressed node in the search tree.
+     *
+     * This object logically addresses two interleaved trees:
+     * a "prefix tree", and a "suffix tree". If you ask for
+     * generic matches, you get both, but if you ask for one
+     * that excludes suffix-only entries, you'll get prefixes
+     * alone.
+     */
+    interface Trie {
+        matches(): RoaringBitmap;
+        substringMatches(): AsyncGenerator<RoaringBitmap>;
+        prefixMatches(): AsyncGenerator<RoaringBitmap>;
+        keys(): Uint8Array;
+        keysExcludeSuffixOnly(): Uint8Array;
+        children(): [number, Promise<Trie>][];
+        childrenExcludeSuffixOnly(): [number, Promise<Trie>][];
+        child(id: number): Promise<Trie>?;
+    }
+    /**
+     * The client interface to Stringdex.
+     */
+    interface DataColumn {
+        isEmpty(id: number): boolean;
+        at(id: number): Promise<Uint8Array|undefined>;
+        length: number,
+    }
+    /**
+     * Callbacks for a host application and VFS backend.
+     *
+     * These functions are calleb with mostly-raw data,
+     * except the JSONP wrapper is removed. For example,
+     * a file with the contents `rr_('{"A":"B"}')` should,
+     * after being pulled in, result in the `rr_` callback
+     * being invoked.
+     *
+     * The success callbacks don't need to supply the name of
+     * the file that succeeded, but, if you want successful error
+     * reporting, you'll need to remember which files are
+     * in flight and report the filename as the first parameter.
+     */
+    interface Callbacks {
+        /**
+         * Load the root of the search database
+         * @param {string} dataString
+         */
+        rr_: function(string);
+        err_rr_: function(any);
+        /**
+         * Load a nodefile in the search tree.
+         * A node file may contain multiple nodes;
+         * each node has five fields, separated by newlines.
+         * @param {string} inputBase64
+         */
+        rn_: function(string);
+        err_rn_: function(string, any);
+        /**
+         * Load a database column partition from a string
+         * @param {string} dataString
+         */
+        rd_: function(string);
+        err_rd_: function(string, any);
+        /**
+         * Load a database column partition from base64
+         * @param {string} dataString
+         */
+        rb_: function(string);
+        err_rb_: function(string, any);
+    };
+    /**
+     * Hooks that a VFS layer must provide for stringdex to load data.
+     *
+     * When the root is loaded, the Callbacks object is provided. These
+     * functions should result in callback functions being called with
+     * the contents of the file, or in error callbacks being invoked with
+     * the failed-to-load filename.
+     */
+    interface Hooks {
+        /**
+         * The first function invoked as part of loading a search database.
+         * This function must, eventually, invoke `rr_` with the string
+         * representation of the root file (the function call wrapper,
+         * `rr_('` and `')`, must be removed).
+         *
+         * The supplied callbacks object is used to feed search data back
+         * to the search engine core. You have to store it, so that
+         * loadTreeByHash and loadDataByNameAndHash can use it.
+         *
+         * If this fails, either throw an exception, or call `err_rr_`
+         * with the error object.
+         */
+        loadRoot: function(Callbacks);
+        /**
+         * Load a subtree file from the search index.
+         * 
+         * If this function succeeds, call `rn_` on the callbacks
+         * object. If it fails, call `err_rn_(hashHex, error)`.
+         * 
+         * @param {string} hashHex
+         */
+        loadTreeByHash: function(string);
+        /**
+         * Load a column partition from the search database.
+         *
+         * If this function succeeds, call `rd_` or `rb_` on the callbacks
+         * object. If it fails, call `err_rd_(hashHex, error)`. or `err_rb_`.
+         * To determine which one, the wrapping function call in the js file
+         * specifies it.
+         *
+         * @param {string} columnName
+         * @param {string} hashHex
+         */
+        loadDataByNameAndHash: function(string, string);
+    };
+    class RoaringBitmap {
+        constructor(array: Uint8Array|null, start?: number);
+        static makeSingleton(number: number);
+        static everything(): RoaringBitmap;
+        static empty(): RoaringBitmap;
+        isEmpty(): boolean;
+        union(that: RoaringBitmap): RoaringBitmap;
+        intersection(that: RoaringBitmap): RoaringBitmap;
+        contains(number: number): boolean;
+        entries(): Generator<number>;
+        first(): number|null;
+        consumed_len_bytes: number;
+    };
+
+    type Stringdex = {
+        /**
+         * Initialize Stringdex with VFS hooks.
+         * Returns a database that you can use.
+         */
+        loadDatabase: function(Hooks): Promise<Database>,
+    };
+
+    const Stringdex: Stringdex;
+    const RoaringBitmap: Class<stringdex.RoaringBitmap>;
+}
+
+declare global {
+    interface Window {
+        Stringdex: stringdex.Stringdex;
+        RoaringBitmap: Class<stringdex.RoaringBitmap>;
+        StringdexOnload: Array<function(stringdex.Stringdex): any>?;
+    };
+}
\ No newline at end of file
diff --git a/src/librustdoc/html/static/js/stringdex.js b/src/librustdoc/html/static/js/stringdex.js
new file mode 100644
index 00000000000..cb956d926db
--- /dev/null
+++ b/src/librustdoc/html/static/js/stringdex.js
@@ -0,0 +1,3217 @@
+/**
+ * @import * as stringdex from "./stringdex.d.ts"
+ */
+
+const EMPTY_UINT8 = new Uint8Array();
+
+/**
+ * @property {Uint8Array} keysAndCardinalities
+ * @property {Uint8Array[]} containers
+ */
+class RoaringBitmap {
+    /**
+     * @param {Uint8Array|null} u8array
+     * @param {number} [startingOffset]
+    */
+    constructor(u8array, startingOffset) {
+        const start = startingOffset ? startingOffset : 0;
+        let i = start;
+        /** @type {Uint8Array} */
+        this.keysAndCardinalities = EMPTY_UINT8;
+        /** @type {(RoaringBitmapArray|RoaringBitmapBits|RoaringBitmapRun)[]} */
+        this.containers = [];
+        /** @type {number} */
+        this.consumed_len_bytes = 0;
+        if (u8array === null || u8array.length === i || u8array[i] === 0) {
+            return this;
+        } else if (u8array[i] > 0xf0) {
+            // Special representation of tiny sets that are close together
+            const lspecial = u8array[i] & 0x0f;
+            this.keysAndCardinalities = new Uint8Array(lspecial * 4);
+            let pspecial = i + 1;
+            let key = u8array[pspecial + 2] | (u8array[pspecial + 3] << 8);
+            let value = u8array[pspecial] | (u8array[pspecial + 1] << 8);
+            let entry = (key << 16) | value;
+            let container;
+            container = new RoaringBitmapArray(1, new Uint8Array(4));
+            container.array[0] = value & 0xFF;
+            container.array[1] = (value >> 8) & 0xFF;
+            this.containers.push(container);
+            this.keysAndCardinalities[0] = key;
+            this.keysAndCardinalities[1] = key >> 8;
+            pspecial += 4;
+            for (let ispecial = 1; ispecial < lspecial; ispecial += 1) {
+                entry += u8array[pspecial] | (u8array[pspecial + 1] << 8);
+                value = entry & 0xffff;
+                key = entry >> 16;
+                container = this.addToArrayAt(key);
+                const cardinalityOld = container.cardinality;
+                container.array[cardinalityOld * 2] = value & 0xFF;
+                container.array[(cardinalityOld * 2) + 1] = (value >> 8) & 0xFF;
+                container.cardinality = cardinalityOld + 1;
+                pspecial += 2;
+            }
+            this.consumed_len_bytes = pspecial - i;
+            return this;
+        } else if (u8array[i] < 0x3a) {
+            // Special representation of tiny sets with arbitrary 32-bit integers
+            const lspecial = u8array[i];
+            this.keysAndCardinalities = new Uint8Array(lspecial * 4);
+            let pspecial = i + 1;
+            for (let ispecial = 0; ispecial < lspecial; ispecial += 1) {
+                const key = u8array[pspecial + 2] | (u8array[pspecial + 3] << 8);
+                const value = u8array[pspecial] | (u8array[pspecial + 1] << 8);
+                const container = this.addToArrayAt(key);
+                const cardinalityOld = container.cardinality;
+                container.array[cardinalityOld * 2] = value & 0xFF;
+                container.array[(cardinalityOld * 2) + 1] = (value >> 8) & 0xFF;
+                container.cardinality = cardinalityOld + 1;
+                pspecial += 4;
+            }
+            this.consumed_len_bytes = pspecial - i;
+            return this;
+        }
+        // https://github.com/RoaringBitmap/RoaringFormatSpec
+        //
+        // Roaring bitmaps are used for flags that can be kept in their
+        // compressed form, even when loaded into memory. This decoder
+        // turns the containers into objects, but uses byte array
+        // slices of the original format for the data payload.
+        const has_runs = u8array[i] === 0x3b;
+        if (u8array[i] !== 0x3a && u8array[i] !== 0x3b) {
+            throw new Error("not a roaring bitmap: " + u8array[i]);
+        }
+        const size = has_runs ?
+            ((u8array[i + 2] | (u8array[i + 3] << 8)) + 1) :
+            ((u8array[i + 4] | (u8array[i + 5] << 8) |
+             (u8array[i + 6] << 16) | (u8array[i + 7] << 24)));
+        i += has_runs ? 4 : 8;
+        let is_run;
+        if (has_runs) {
+            const is_run_len = (size + 7) >> 3;
+            is_run = new Uint8Array(u8array.buffer, i + u8array.byteOffset, is_run_len);
+            i += is_run_len;
+        } else {
+            is_run = EMPTY_UINT8;
+        }
+        this.keysAndCardinalities = u8array.subarray(i, i + (size * 4));
+        i += size * 4;
+        let offsets = null;
+        if (!has_runs || size >= 4) {
+            offsets = [];
+            for (let j = 0; j < size; ++j) {
+                offsets.push(u8array[i] | (u8array[i + 1] << 8) | (u8array[i + 2] << 16) |
+                    (u8array[i + 3] << 24));
+                i += 4;
+            }
+        }
+        for (let j = 0; j < size; ++j) {
+            if (offsets && offsets[j] !== i - start) {
+                throw new Error(`corrupt bitmap ${j}: ${i - start} / ${offsets[j]}`);
+            }
+            const cardinality = (this.keysAndCardinalities[(j * 4) + 2] |
+                (this.keysAndCardinalities[(j * 4) + 3] << 8)) + 1;
+            if (is_run[j >> 3] & (1 << (j & 0x7))) {
+                const runcount = (u8array[i] | (u8array[i + 1] << 8));
+                i += 2;
+                this.containers.push(new RoaringBitmapRun(
+                    runcount,
+                    new Uint8Array(u8array.buffer, i + u8array.byteOffset, runcount * 4),
+                ));
+                i += runcount * 4;
+            } else if (cardinality >= 4096) {
+                this.containers.push(new RoaringBitmapBits(new Uint8Array(
+                    u8array.buffer,
+                    i + u8array.byteOffset, 8192,
+                )));
+                i += 8192;
+            } else {
+                const end = cardinality * 2;
+                this.containers.push(new RoaringBitmapArray(
+                    cardinality,
+                    new Uint8Array(u8array.buffer, i + u8array.byteOffset, end),
+                ));
+                i += end;
+            }
+        }
+        this.consumed_len_bytes = i - start;
+    }
+    /**
+     * @param {number} number
+     * @returns {RoaringBitmap}
+     */
+    static makeSingleton(number) {
+        const result = new RoaringBitmap(null, 0);
+        result.keysAndCardinalities = Uint8Array.of(
+            (number >> 16), (number >> 24),
+            0, 0, // keysAndCardinalities stores the true cardinality minus 1
+        );
+        result.containers.push(new RoaringBitmapArray(
+            1,
+            Uint8Array.of(number, number >> 8),
+        ));
+        return result;
+    }
+    /** @returns {RoaringBitmap} */
+    static everything() {
+        if (EVERYTHING_BITMAP.isEmpty()) {
+            let i = 0;
+            const l = 1 << 16;
+            const everything_range = new RoaringBitmapRun(1, Uint8Array.of(0, 0, 0xff, 0xff));
+            EVERYTHING_BITMAP.keysAndCardinalities = new Uint8Array(l * 4);
+            while (i < l) {
+                EVERYTHING_BITMAP.containers.push(everything_range);
+                // key
+                EVERYTHING_BITMAP.keysAndCardinalities[(i * 4) + 0] = i;
+                EVERYTHING_BITMAP.keysAndCardinalities[(i * 4) + 1] = i >> 8;
+                // cardinality (minus one)
+                EVERYTHING_BITMAP.keysAndCardinalities[(i * 4) + 2] = 0xff;
+                EVERYTHING_BITMAP.keysAndCardinalities[(i * 4) + 3] = 0xff;
+                i += 1;
+            }
+        }
+        return EVERYTHING_BITMAP;
+    }
+    /** @returns {RoaringBitmap} */
+    static empty() {
+        return EMPTY_BITMAP;
+    }
+    /** @returns {boolean} */
+    isEmpty() {
+        return this.containers.length === 0;
+    }
+    /**
+     * Helper function used when constructing bitmaps from lists.
+     * Returns an array container with at least two free byte slots
+     * and bumps `this.cardinalities`.
+     * @param {number} key
+     * @returns {RoaringBitmapArray}
+     */
+    addToArrayAt(key) {
+        let mid = this.getContainerId(key);
+        /** @type {RoaringBitmapArray|RoaringBitmapBits|RoaringBitmapRun} */
+        let container;
+        if (mid === -1) {
+            container = new RoaringBitmapArray(0, new Uint8Array(2));
+            mid = this.containers.length;
+            this.containers.push(container);
+            if (mid * 4 > this.keysAndCardinalities.length) {
+                const keysAndContainers = new Uint8Array(mid * 8);
+                keysAndContainers.set(this.keysAndCardinalities);
+                this.keysAndCardinalities = keysAndContainers;
+            }
+            this.keysAndCardinalities[(mid * 4) + 0] = key;
+            this.keysAndCardinalities[(mid * 4) + 1] = key >> 8;
+        } else {
+            container = this.containers[mid];
+            const cardinalityOld =
+                this.keysAndCardinalities[(mid * 4) + 2] |
+                (this.keysAndCardinalities[(mid * 4) + 3] << 8);
+            const cardinality = cardinalityOld + 1;
+            this.keysAndCardinalities[(mid * 4) + 2] = cardinality;
+            this.keysAndCardinalities[(mid * 4) + 3] = cardinality >> 8;
+        }
+        // the logic for handing this number is annoying, because keysAndCardinalities stores
+        // the cardinality *minus one*, so that it can count up to 65536 with only two bytes
+        // (because empty containers are never stored).
+        //
+        // So, if this is a new container, the stored cardinality contains `0 0`, which is
+        // the proper value of the old cardinality (an imaginary empty container existed).
+        // If this is adding to an existing container, then the above `else` branch bumps it
+        // by one, leaving us with a proper value of `cardinality - 1`.
+        const cardinalityOld =
+            this.keysAndCardinalities[(mid * 4) + 2] |
+            (this.keysAndCardinalities[(mid * 4) + 3] << 8);
+        if (!(container instanceof RoaringBitmapArray) ||
+            container.array.byteLength < ((cardinalityOld + 1) * 2)
+        ) {
+            const newBuf = new Uint8Array((cardinalityOld + 1) * 4);
+            let idx = 0;
+            for (const cvalue of container.values()) {
+                newBuf[idx] = cvalue & 0xFF;
+                newBuf[idx + 1] = (cvalue >> 8) & 0xFF;
+                idx += 2;
+            }
+            if (container instanceof RoaringBitmapArray) {
+                container.cardinality = cardinalityOld;
+                container.array = newBuf;
+                return container;
+            }
+            const newcontainer = new RoaringBitmapArray(cardinalityOld, newBuf);
+            this.containers[mid] = newcontainer;
+            return newcontainer;
+        } else {
+            return container;
+        }
+    }
+    /**
+     * @param {RoaringBitmap} that
+     * @returns {RoaringBitmap}
+     */
+    union(that) {
+        if (this.isEmpty()) {
+            return that;
+        }
+        if (that.isEmpty()) {
+            return this;
+        }
+        if (this === RoaringBitmap.everything() || that === RoaringBitmap.everything()) {
+            return RoaringBitmap.everything();
+        }
+        let i = 0;
+        const il = this.containers.length;
+        let j = 0;
+        const jl = that.containers.length;
+        const result = new RoaringBitmap(null, 0);
+        result.keysAndCardinalities = new Uint8Array((il + jl) * 4);
+        while (i < il || j < jl) {
+            const ik = i * 4;
+            const jk = j * 4;
+            const k = result.containers.length * 4;
+            if (j >= jl || (i < il && (
+                (this.keysAndCardinalities[ik + 1] < that.keysAndCardinalities[jk + 1]) ||
+                (this.keysAndCardinalities[ik + 1] === that.keysAndCardinalities[jk + 1] &&
+                    this.keysAndCardinalities[ik] < that.keysAndCardinalities[jk])
+            ))) {
+                result.keysAndCardinalities[k + 0] = this.keysAndCardinalities[ik + 0];
+                result.keysAndCardinalities[k + 1] = this.keysAndCardinalities[ik + 1];
+                result.keysAndCardinalities[k + 2] = this.keysAndCardinalities[ik + 2];
+                result.keysAndCardinalities[k + 3] = this.keysAndCardinalities[ik + 3];
+                result.containers.push(this.containers[i]);
+                i += 1;
+            } else if (i >= il || (j < jl && (
+                (that.keysAndCardinalities[jk + 1] < this.keysAndCardinalities[ik + 1]) ||
+                (that.keysAndCardinalities[jk + 1] === this.keysAndCardinalities[ik + 1] &&
+                    that.keysAndCardinalities[jk] < this.keysAndCardinalities[ik])
+            ))) {
+                result.keysAndCardinalities[k + 0] = that.keysAndCardinalities[jk + 0];
+                result.keysAndCardinalities[k + 1] = that.keysAndCardinalities[jk + 1];
+                result.keysAndCardinalities[k + 2] = that.keysAndCardinalities[jk + 2];
+                result.keysAndCardinalities[k + 3] = that.keysAndCardinalities[jk + 3];
+                result.containers.push(that.containers[j]);
+                j += 1;
+            } else {
+                // this key is not smaller than that key
+                // that key is not smaller than this key
+                // they must be equal
+                const thisContainer = this.containers[i];
+                const thatContainer = that.containers[j];
+                let card = 0;
+                if (thisContainer instanceof RoaringBitmapBits &&
+                    thatContainer instanceof RoaringBitmapBits
+                ) {
+                    const resultArray = new Uint8Array(
+                        thisContainer.array.length > thatContainer.array.length ?
+                            thisContainer.array.length :
+                            thatContainer.array.length,
+                    );
+                    let k = 0;
+                    const kl = resultArray.length;
+                    while (k < kl) {
+                        const c = thisContainer.array[k] | thatContainer.array[k];
+                        resultArray[k] = c;
+                        card += bitCount(c);
+                        k += 1;
+                    }
+                    result.containers.push(new RoaringBitmapBits(resultArray));
+                } else {
+                    const thisValues = thisContainer.values();
+                    const thatValues = thatContainer.values();
+                    let thisResult = thisValues.next();
+                    let thatResult = thatValues.next();
+                    /** @type {Array<number>} */
+                    const resultValues = [];
+                    while (!thatResult.done || !thisResult.done) {
+                        // generator will definitely implement the iterator protocol correctly
+                        /** @type {number} */
+                        const thisValue = thisResult.value;
+                        /** @type {number} */
+                        const thatValue = thatResult.value;
+                        if (thatResult.done || thisValue < thatValue) {
+                            resultValues.push(thisValue);
+                            thisResult = thisValues.next();
+                        } else if (thisResult.done || thatValue < thisValue) {
+                            resultValues.push(thatValue);
+                            thatResult = thatValues.next();
+                        } else {
+                            // this value is not smaller than that value
+                            // that value is not smaller than this value
+                            // they must be equal
+                            resultValues.push(thisValue);
+                            thisResult = thisValues.next();
+                            thatResult = thatValues.next();
+                        }
+                    }
+                    const resultArray = new Uint8Array(resultValues.length * 2);
+                    let k = 0;
+                    for (const value of resultValues) {
+                        // roaring bitmap is little endian
+                        resultArray[k] = value & 0xFF;
+                        resultArray[k + 1] = (value >> 8) & 0xFF;
+                        k += 2;
+                    }
+                    result.containers.push(new RoaringBitmapArray(
+                        resultValues.length,
+                        resultArray,
+                    ));
+                    card = resultValues.length;
+                }
+                result.keysAndCardinalities[k + 0] = this.keysAndCardinalities[ik + 0];
+                result.keysAndCardinalities[k + 1] = this.keysAndCardinalities[ik + 1];
+                card -= 1;
+                result.keysAndCardinalities[k + 2] = card;
+                result.keysAndCardinalities[k + 3] = card >> 8;
+                i += 1;
+                j += 1;
+            }
+        }
+        return result;
+    }
+    /**
+     * @param {RoaringBitmap} that
+     * @returns {RoaringBitmap}
+     */
+    intersection(that) {
+        if (this.isEmpty() || that.isEmpty()) {
+            return EMPTY_BITMAP;
+        }
+        if (this === RoaringBitmap.everything()) {
+            return that;
+        }
+        if (that === RoaringBitmap.everything()) {
+            return this;
+        }
+        let i = 0;
+        const il = this.containers.length;
+        let j = 0;
+        const jl = that.containers.length;
+        const result = new RoaringBitmap(null, 0);
+        result.keysAndCardinalities = new Uint8Array((il > jl ? il : jl) * 4);
+        while (i < il && j < jl) {
+            const ik = i * 4;
+            const jk = j * 4;
+            const k = result.containers.length * 4;
+            if (j >= jl || (i < il && (
+                (this.keysAndCardinalities[ik + 1] < that.keysAndCardinalities[jk + 1]) ||
+                (this.keysAndCardinalities[ik + 1] === that.keysAndCardinalities[jk + 1] &&
+                    this.keysAndCardinalities[ik] < that.keysAndCardinalities[jk])
+            ))) {
+                i += 1;
+            } else if (i >= il || (j < jl && (
+                (that.keysAndCardinalities[jk + 1] < this.keysAndCardinalities[ik + 1]) ||
+                (that.keysAndCardinalities[jk + 1] === this.keysAndCardinalities[ik + 1] &&
+                    that.keysAndCardinalities[jk] < this.keysAndCardinalities[ik])
+            ))) {
+                j += 1;
+            } else {
+                // this key is not smaller than that key
+                // that key is not smaller than this key
+                // they must be equal
+                const thisContainer = this.containers[i];
+                const thatContainer = that.containers[j];
+                let card = 0;
+                if (thisContainer instanceof RoaringBitmapBits &&
+                    thatContainer instanceof RoaringBitmapBits
+                ) {
+                    const resultArray = new Uint8Array(
+                        thisContainer.array.length > thatContainer.array.length ?
+                            thisContainer.array.length :
+                            thatContainer.array.length,
+                    );
+                    let k = 0;
+                    const kl = resultArray.length;
+                    while (k < kl) {
+                        const c = thisContainer.array[k] & thatContainer.array[k];
+                        resultArray[k] = c;
+                        card += bitCount(c);
+                        k += 1;
+                    }
+                    if (card !== 0) {
+                        result.containers.push(new RoaringBitmapBits(resultArray));
+                    }
+                } else {
+                    const thisValues = thisContainer.values();
+                    const thatValues = thatContainer.values();
+                    let thisValue = thisValues.next();
+                    let thatValue = thatValues.next();
+                    const resultValues = [];
+                    while (!thatValue.done && !thisValue.done) {
+                        if (thisValue.value < thatValue.value) {
+                            thisValue = thisValues.next();
+                        } else if (thatValue.value < thisValue.value) {
+                            thatValue = thatValues.next();
+                        } else {
+                            // this value is not smaller than that value
+                            // that value is not smaller than this value
+                            // they must be equal
+                            resultValues.push(thisValue.value);
+                            thisValue = thisValues.next();
+                            thatValue = thatValues.next();
+                        }
+                    }
+                    card = resultValues.length;
+                    if (card !== 0) {
+                        const resultArray = new Uint8Array(resultValues.length * 2);
+                        let k = 0;
+                        for (const value of resultValues) {
+                            // roaring bitmap is little endian
+                            resultArray[k] = value & 0xFF;
+                            resultArray[k + 1] = (value >> 8) & 0xFF;
+                            k += 2;
+                        }
+                        result.containers.push(new RoaringBitmapArray(
+                            resultValues.length,
+                            resultArray,
+                        ));
+                    }
+                }
+                if (card !== 0) {
+                    result.keysAndCardinalities[k + 0] = this.keysAndCardinalities[ik + 0];
+                    result.keysAndCardinalities[k + 1] = this.keysAndCardinalities[ik + 1];
+                    card -= 1;
+                    result.keysAndCardinalities[k + 2] = card;
+                    result.keysAndCardinalities[k + 3] = card >> 8;
+                }
+                i += 1;
+                j += 1;
+            }
+        }
+        return result;
+    }
+    /** @param {number} keyvalue */
+    contains(keyvalue) {
+        const key = keyvalue >> 16;
+        const value = keyvalue & 0xFFFF;
+        const mid = this.getContainerId(key);
+        return mid === -1 ? false : this.containers[mid].contains(value);
+    }
+    /**
+     * @param {number} key
+     * @returns {number}
+     */
+    getContainerId(key) {
+        // Binary search algorithm copied from
+        // https://en.wikipedia.org/wiki/Binary_search#Procedure
+        //
+        // Format is required by specification to be sorted.
+        // Because keys are 16 bits and unique, length can't be
+        // bigger than 2**16, and because we have 32 bits of safe int,
+        // left + right can't overflow.
+        let left = 0;
+        let right = this.containers.length - 1;
+        while (left <= right) {
+            const mid = Math.floor((left + right) / 2);
+            const x = this.keysAndCardinalities[(mid * 4)] |
+                (this.keysAndCardinalities[(mid * 4) + 1] << 8);
+            if (x < key) {
+                left = mid + 1;
+            } else if (x > key) {
+                right = mid - 1;
+            } else {
+                return mid;
+            }
+        }
+        return -1;
+    }
+    * entries() {
+        const l = this.containers.length;
+        for (let i = 0; i < l; ++i) {
+            const key = this.keysAndCardinalities[i * 4] |
+                (this.keysAndCardinalities[(i * 4) + 1] << 8);
+            for (const value of this.containers[i].values()) {
+                yield (key << 16) | value;
+            }
+        }
+    }
+    /**
+     * @returns {number|null}
+     */
+    first() {
+        for (const entry of this.entries()) {
+            return entry;
+        }
+        return null;
+    }
+    /**
+     * @returns {number}
+     */
+    cardinality() {
+        let result = 0;
+        const l = this.containers.length;
+        for (let i = 0; i < l; ++i) {
+            const card = this.keysAndCardinalities[(i * 4) + 2] |
+                (this.keysAndCardinalities[(i * 4) + 3] << 8);
+            result += card + 1;
+        }
+        return result;
+    }
+}
+
+class RoaringBitmapRun {
+    /**
+     * @param {number} runcount
+     * @param {Uint8Array} array
+     */
+    constructor(runcount, array) {
+        this.runcount = runcount;
+        this.array = array;
+    }
+    /** @param {number} value */
+    contains(value) {
+        // Binary search algorithm copied from
+        // https://en.wikipedia.org/wiki/Binary_search#Procedure
+        //
+        // Since runcount is stored as 16 bits, left + right
+        // can't overflow.
+        let left = 0;
+        let right = this.runcount - 1;
+        while (left <= right) {
+            const mid = (left + right) >> 1;
+            const i = mid * 4;
+            const start = this.array[i] | (this.array[i + 1] << 8);
+            const lenm1 = this.array[i + 2] | (this.array[i + 3] << 8);
+            if ((start + lenm1) < value) {
+                left = mid + 1;
+            } else if (start > value) {
+                right = mid - 1;
+            } else {
+                return true;
+            }
+        }
+        return false;
+    }
+    * values() {
+        let i = 0;
+        while (i < this.runcount) {
+            const start = this.array[i * 4] | (this.array[(i * 4) + 1] << 8);
+            const lenm1 = this.array[(i * 4) + 2] | (this.array[(i * 4) + 3] << 8);
+            let value = start;
+            let j = 0;
+            while (j <= lenm1) {
+                yield value;
+                value += 1;
+                j += 1;
+            }
+            i += 1;
+        }
+    }
+}
+class RoaringBitmapArray {
+    /**
+     * @param {number} cardinality
+     * @param {Uint8Array} array
+     */
+    constructor(cardinality, array) {
+        this.cardinality = cardinality;
+        this.array = array;
+    }
+    /** @param {number} value */
+    contains(value) {
+        // Binary search algorithm copied from
+        // https://en.wikipedia.org/wiki/Binary_search#Procedure
+        //
+        // Since cardinality can't be higher than 4096, left + right
+        // cannot overflow.
+        let left = 0;
+        let right = this.cardinality - 1;
+        while (left <= right) {
+            const mid = (left + right) >> 1;
+            const i = mid * 2;
+            const x = this.array[i] | (this.array[i + 1] << 8);
+            if (x < value) {
+                left = mid + 1;
+            } else if (x > value) {
+                right = mid - 1;
+            } else {
+                return true;
+            }
+        }
+        return false;
+    }
+    /** @returns {Generator<number>} */
+    * values() {
+        let i = 0;
+        const l = this.cardinality * 2;
+        while (i < l) {
+            yield this.array[i] | (this.array[i + 1] << 8);
+            i += 2;
+        }
+    }
+}
+class RoaringBitmapBits {
+    /**
+     * @param {Uint8Array} array
+     */
+    constructor(array) {
+        this.array = array;
+    }
+    /** @param {number} value */
+    contains(value) {
+        return !!(this.array[value >> 3] & (1 << (value & 7)));
+    }
+    * values() {
+        let i = 0;
+        const l = this.array.length << 3;
+        while (i < l) {
+            if (this.contains(i)) {
+                yield i;
+            }
+            i += 1;
+        }
+    }
+}
+
+const EMPTY_BITMAP = new RoaringBitmap(null, 0);
+EMPTY_BITMAP.consumed_len_bytes = 0;
+const EMPTY_BITMAP1 = new RoaringBitmap(null, 0);
+EMPTY_BITMAP1.consumed_len_bytes = 1;
+const EVERYTHING_BITMAP = new RoaringBitmap(null, 0);
+
+/**
+ * A mapping from six byte nodeids to an arbitrary value.
+ * We don't just use `Map` because that requires double hashing.
+ * @template T
+ * @property {Uint8Array} keys
+ * @property {T[]} values
+ * @property {number} size
+ * @property {number} capacityClass
+ */
+class HashTable {
+    /**
+     * Construct an empty hash table.
+     */
+    constructor() {
+        this.keys = EMPTY_UINT8;
+        /** @type {(T|undefined)[]} */
+        this.values = [];
+        this.size = 0;
+        this.capacityClass = 0;
+    }
+    /**
+     * @returns {Generator<[Uint8Array, T]>}
+     */
+    * entries() {
+        const keys = this.keys;
+        const values = this.values;
+        const l = this.values.length;
+        for (let i = 0; i < l; i += 1) {
+            const value = values[i];
+            if (value !== undefined) {
+                yield [keys.subarray(i * 6, (i + 1) * 6), value];
+            }
+        }
+    }
+    /**
+     * Add a value to the hash table.
+     * @param {Uint8Array} key
+     * @param {T} value
+     */
+    set(key, value) {
+        // 90 % load factor
+        if (this.size * 10 >= this.values.length * 9) {
+            const keys = this.keys;
+            const values = this.values;
+            const l = values.length;
+            this.capacityClass += 1;
+            const capacity = 1 << this.capacityClass;
+            this.keys = new Uint8Array(capacity * 6);
+            this.values = [];
+            for (let i = 0; i < capacity; i += 1) {
+                this.values.push(undefined);
+            }
+            this.size = 0;
+            for (let i = 0; i < l; i += 1) {
+                const oldValue = values[i];
+                if (oldValue !== undefined) {
+                    this.setNoGrow(keys, i * 6, oldValue);
+                }
+            }
+        }
+        this.setNoGrow(key, 0, value);
+    }
+    /**
+     * @param {Uint8Array} key
+     * @param {number} start
+     * @param {T} value
+     */
+    setNoGrow(key, start, value) {
+        const mask = ~(0xffffffff << this.capacityClass);
+        const keys = this.keys;
+        const values = this.values;
+        const l = 1 << this.capacityClass;
+        // because we know that our values are already hashed,
+        // just chop off the lower four bytes
+        let slot = (
+            (key[start + 2] << 24) |
+            (key[start + 3] << 16) |
+            (key[start + 4] << 8) |
+            key[start + 5]
+        ) & mask;
+        for (let distance = 0; distance < l; ) {
+            const j = slot * 6;
+            const otherValue = values[slot];
+            if (otherValue === undefined) {
+                values[slot] = value;
+                const keysStart = slot * 6;
+                keys[keysStart + 0] = key[start + 0];
+                keys[keysStart + 1] = key[start + 1];
+                keys[keysStart + 2] = key[start + 2];
+                keys[keysStart + 3] = key[start + 3];
+                keys[keysStart + 4] = key[start + 4];
+                keys[keysStart + 5] = key[start + 5];
+                this.size += 1;
+                break;
+            } else if (
+                key[start + 0] === keys[j + 0] &&
+                key[start + 1] === keys[j + 1] &&
+                key[start + 2] === keys[j + 2] &&
+                key[start + 3] === keys[j + 3] &&
+                key[start + 4] === keys[j + 4] &&
+                key[start + 5] === keys[j + 5]
+            ) {
+                values[slot] = value;
+                break;
+            } else {
+                const otherPreferredSlot = (
+                    (keys[j + 2] << 24) | (keys[j + 3] << 16) |
+                    (keys[j + 4] << 8) | keys[j + 5]
+                ) & mask;
+                const otherDistance = otherPreferredSlot <= slot ?
+                    slot - otherPreferredSlot :
+                    (l - otherPreferredSlot) + slot;
+                if (distance > otherDistance) {
+                    // if the other key is closer to its preferred slot than this one,
+                    // then insert our node in its place and swap
+                    //
+                    // https://cglab.ca/~abeinges/blah/robinhood-part-1/
+                    const otherKey = keys.slice(j, j + 6);
+                    values[slot] = value;
+                    value = otherValue;
+                    keys[j + 0] = key[start + 0];
+                    keys[j + 1] = key[start + 1];
+                    keys[j + 2] = key[start + 2];
+                    keys[j + 3] = key[start + 3];
+                    keys[j + 4] = key[start + 4];
+                    keys[j + 5] = key[start + 5];
+                    key = otherKey;
+                    start = 0;
+                    distance = otherDistance;
+                }
+                distance += 1;
+                slot = (slot + 1) & mask;
+            }
+        }
+    }
+    /**
+     * Retrieve a value
+     * @param {Uint8Array} key
+     * @returns {T|undefined}
+     */
+    get(key) {
+        if (key.length !== 6) {
+            throw "invalid key";
+        }
+        return this.getWithOffsetKey(key, 0);
+    }
+    /**
+     * Retrieve a value
+     * @param {Uint8Array} key
+     * @param {number} start
+     * @returns {T|undefined}
+     */
+    getWithOffsetKey(key, start) {
+        const mask = ~(0xffffffff << this.capacityClass);
+        const keys = this.keys;
+        const values = this.values;
+        const l = 1 << this.capacityClass;
+        // because we know that our values are already hashed,
+        // just chop off the lower four bytes
+        let slot = (
+            (key[start + 2] << 24) |
+            (key[start + 3] << 16) |
+            (key[start + 4] << 8) |
+            key[start + 5]
+        ) & mask;
+        for (let distance = 0; distance < l; distance += 1) {
+            const j = slot * 6;
+            const value = values[slot];
+            if (value === undefined) {
+                break;
+            } else if (
+                key[start + 0] === keys[j + 0] &&
+                key[start + 1] === keys[j + 1] &&
+                key[start + 2] === keys[j + 2] &&
+                key[start + 3] === keys[j + 3] &&
+                key[start + 4] === keys[j + 4] &&
+                key[start + 5] === keys[j + 5]
+            ) {
+                return value;
+            } else {
+                const otherPreferredSlot = (
+                    (keys[j + 2] << 24) | (keys[j + 3] << 16) |
+                    (keys[j + 4] << 8) | keys[j + 5]
+                ) & mask;
+                const otherDistance = otherPreferredSlot <= slot ?
+                    slot - otherPreferredSlot :
+                    (l - otherPreferredSlot) + slot;
+                if (distance > otherDistance) {
+                    break;
+                }
+            }
+            slot = (slot + 1) & mask;
+        }
+        return undefined;
+    }
+}
+
+/*eslint-disable */
+// ignore-tidy-linelength
+/** <https://stackoverflow.com/questions/43122082/efficiently-count-the-number-of-bits-in-an-integer-in-javascript>
+ * @param {number} n
+ * @returns {number}
+ */
+function bitCount(n) {
+    n = (~~n) - ((n >> 1) & 0x55555555);
+    n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
+    return ((n + (n >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
+}
+/*eslint-enable */
+
+/**
+ * @param {stringdex.Hooks} hooks
+ * @returns {Promise<stringdex.Database>}
+ */
+function loadDatabase(hooks) {
+    /** @type {stringdex.Callbacks} */
+    const callbacks = {
+        rr_: function(data) {
+            const dataObj = JSON.parse(data);
+            for (const colName of Object.keys(dataObj)) {
+                if (Object.hasOwn(dataObj[colName], "I")) {
+                    registry.searchTreeRoots.set(
+                        colName,
+                        makeSearchTreeFromBase64(dataObj[colName].I)[1],
+                    );
+                }
+                if (Object.hasOwn(dataObj[colName], "N")) {
+                    const counts = [];
+                    const countsstring = dataObj[colName]["N"];
+                    let i = 0;
+                    const l = countsstring.length;
+                    while (i < l) {
+                        let n = 0;
+                        let c = countsstring.charCodeAt(i);
+                        while (c < 96) { // 96 = "`"
+                            n = (n << 4) | (c & 0xF);
+                            i += 1;
+                            c = countsstring.charCodeAt(i);
+                        }
+                        n = (n << 4) | (c & 0xF);
+                        counts.push(n);
+                        i += 1;
+                    }
+                    registry.dataColumns.set(colName, new DataColumn(
+                        counts,
+                        makeUint8ArrayFromBase64(dataObj[colName]["H"]),
+                        new RoaringBitmap(makeUint8ArrayFromBase64(dataObj[colName]["E"]), 0),
+                        colName,
+                    ));
+                }
+            }
+            const cb = registry.searchTreeRootCallback;
+            if (cb) {
+                cb(null, new Database(registry.searchTreeRoots, registry.dataColumns));
+            }
+        },
+        err_rr_: function(err) {
+            const cb = registry.searchTreeRootCallback;
+            if (cb) {
+                cb(err, null);
+            }
+        },
+        rd_: function(dataString) {
+            const l = dataString.length;
+            const data = new Uint8Array(l);
+            for (let i = 0; i < l; ++i) {
+                data[i] = dataString.charCodeAt(i);
+            }
+            loadColumnFromBytes(data);
+        },
+        err_rd_: function(filename, err) {
+            const nodeid = makeUint8ArrayFromHex(filename);
+            const cb = registry.dataColumnLoadPromiseCallbacks.get(nodeid);
+            if (cb) {
+                cb(err, null);
+            }
+        },
+        rb_: function(dataString64) {
+            loadColumnFromBytes(makeUint8ArrayFromBase64(dataString64));
+        },
+        err_rb_: function(filename, err) {
+            const nodeid = makeUint8ArrayFromHex(filename);
+            const cb = registry.dataColumnLoadPromiseCallbacks.get(nodeid);
+            if (cb) {
+                cb(err, null);
+            }
+        },
+        rn_: function(inputBase64) {
+            const [nodeid, tree] = makeSearchTreeFromBase64(inputBase64);
+            const cb = registry.searchTreeLoadPromiseCallbacks.get(nodeid);
+            if (cb) {
+                cb(null, tree);
+                registry.searchTreeLoadPromiseCallbacks.set(nodeid, null);
+            }
+        },
+        err_rn_: function(filename, err) {
+            const nodeid = makeUint8ArrayFromHex(filename);
+            const cb = registry.searchTreeLoadPromiseCallbacks.get(nodeid);
+            if (cb) {
+                cb(err, null);
+            }
+        },
+    };
+
+    /**
+     * @type {{
+     *      searchTreeRoots: Map<string, SearchTree>;
+     *      searchTreeLoadPromiseCallbacks: HashTable<(function(any, SearchTree?): any)|null>;
+     *      searchTreePromises: HashTable<Promise<SearchTree>>;
+     *      dataColumnLoadPromiseCallbacks: HashTable<function(any, Uint8Array[]?): any>;
+     *      dataColumns: Map<string, DataColumn>;
+     *      dataColumnsBuckets: Map<string, HashTable<Promise<Uint8Array[]>>>;
+     *      searchTreeLoadByNodeID: function(Uint8Array): Promise<SearchTree>;
+     *      searchTreeRootCallback?: function(any, Database?): any;
+     *      dataLoadByNameAndHash: function(string, Uint8Array): Promise<Uint8Array[]>;
+     * }}
+     */
+    const registry = {
+        searchTreeRoots: new Map(),
+        searchTreeLoadPromiseCallbacks: new HashTable(),
+        searchTreePromises: new HashTable(),
+        dataColumnLoadPromiseCallbacks: new HashTable(),
+        dataColumns: new Map(),
+        dataColumnsBuckets: new Map(),
+        searchTreeLoadByNodeID: function(nodeid) {
+            const existingPromise = registry.searchTreePromises.get(nodeid);
+            if (existingPromise) {
+                return existingPromise;
+            }
+            /** @type {Promise<SearchTree>} */
+            let newPromise;
+            if ((nodeid[0] & 0x80) !== 0) {
+                const isWhole = (nodeid[0] & 0x40) !== 0;
+                let leaves;
+                if ((nodeid[0] & 0x10) !== 0) {
+                    let id1 = (nodeid[2] << 8) | nodeid[3];
+                    if ((nodeid[0] & 0x20) !== 0) {
+                        // when data is present, id1 can be up to 20 bits
+                        id1 |= ((nodeid[1] & 0x0f) << 16);
+                    } else {
+                        // otherwise, we fit in 28
+                        id1 |= ((nodeid[0] & 0x0f) << 24) | (nodeid[1] << 16);
+                    }
+                    const id2 = id1 + ((nodeid[4] << 8) | nodeid[5]);
+                    leaves = RoaringBitmap.makeSingleton(id1)
+                        .union(RoaringBitmap.makeSingleton(id2));
+                } else {
+                    leaves = RoaringBitmap.makeSingleton(
+                        (nodeid[2] << 24) | (nodeid[3] << 16) |
+                        (nodeid[4] << 8) | nodeid[5],
+                    );
+                }
+                const data = (nodeid[0] & 0x20) !== 0 ?
+                    Uint8Array.of(((nodeid[0] & 0x0f) << 4) | (nodeid[1] >> 4)) :
+                    EMPTY_UINT8;
+                newPromise = Promise.resolve(new SearchTree(
+                    EMPTY_SEARCH_TREE_BRANCHES,
+                    EMPTY_SEARCH_TREE_BRANCHES,
+                    data,
+                    isWhole ? leaves : EMPTY_BITMAP,
+                    isWhole ? EMPTY_BITMAP : leaves,
+                ));
+            } else {
+                const hashHex = makeHexFromUint8Array(nodeid);
+                newPromise = new Promise((resolve, reject) => {
+                    const cb = registry.searchTreeLoadPromiseCallbacks.get(nodeid);
+                    if (cb) {
+                        registry.searchTreeLoadPromiseCallbacks.set(nodeid, (err, data) => {
+                            cb(err, data);
+                            if (data) {
+                                resolve(data);
+                            } else {
+                                reject(err);
+                            }
+                        });
+                    } else {
+                        registry.searchTreeLoadPromiseCallbacks.set(nodeid, (err, data) => {
+                            if (data) {
+                                resolve(data);
+                            } else {
+                                reject(err);
+                            }
+                        });
+                        hooks.loadTreeByHash(hashHex);
+                    }
+                });
+            }
+            registry.searchTreePromises.set(nodeid, newPromise);
+            return newPromise;
+        },
+        dataLoadByNameAndHash: function(name, hash) {
+            let dataColumnBuckets = registry.dataColumnsBuckets.get(name);
+            if (dataColumnBuckets === undefined) {
+                dataColumnBuckets = new HashTable();
+                registry.dataColumnsBuckets.set(name, dataColumnBuckets);
+            }
+            const existingBucket = dataColumnBuckets.get(hash);
+            if (existingBucket) {
+                return existingBucket;
+            }
+            const hashHex = makeHexFromUint8Array(hash);
+            /** @type {Promise<Uint8Array[]>} */
+            const newBucket = new Promise((resolve, reject) => {
+                const cb = registry.dataColumnLoadPromiseCallbacks.get(hash);
+                if (cb) {
+                    registry.dataColumnLoadPromiseCallbacks.set(hash, (err, data) => {
+                        cb(err, data);
+                        if (data) {
+                            resolve(data);
+                        } else {
+                            reject(err);
+                        }
+                    });
+                } else {
+                    registry.dataColumnLoadPromiseCallbacks.set(hash, (err, data) => {
+                        if (data) {
+                            resolve(data);
+                        } else {
+                            reject(err);
+                        }
+                    });
+                    hooks.loadDataByNameAndHash(name, hashHex);
+                }
+            });
+            dataColumnBuckets.set(hash, newBucket);
+            return newBucket;
+        },
+    };
+
+    /**
+     * The set of child subtrees.
+     * @type {{
+     *    nodeids: Uint8Array,
+     *    subtrees: Array<Promise<SearchTree>|null>,
+     * }}
+     */
+    class SearchTreeBranches {
+        /**
+         * Construct the subtree list with `length` nulls
+         * @param {number} length
+         * @param {Uint8Array} nodeids
+         */
+        constructor(length, nodeids) {
+            this.nodeids = nodeids;
+            this.subtrees = [];
+            for (let i = 0; i < length; ++i) {
+                this.subtrees.push(null);
+            }
+        }
+        /**
+         * @param {number} i
+         * @returns {Uint8Array}
+        */
+        getNodeID(i) {
+            return new Uint8Array(
+                this.nodeids.buffer,
+                this.nodeids.byteOffset + (i * 6),
+                6,
+            );
+        }
+        // https://github.com/microsoft/TypeScript/issues/17227
+        /** @returns {Generator<[number, Promise<SearchTree>|null]>} */
+        entries() {
+            throw new Error();
+        }
+        /**
+         * @param {number} _k
+         * @returns {number}
+         */
+        getIndex(_k) {
+            throw new Error();
+        }
+        /**
+         * @param {number} _i
+         * @returns {number}
+         */
+        getKey(_i) {
+            throw new Error();
+        }
+        /**
+         * @returns {Uint8Array}
+         */
+        getKeys() {
+            throw new Error();
+        }
+    }
+
+    /**
+     * A sorted array of search tree branches.
+     *
+     * @type {{
+     *    keys: Uint8Array,
+     *    nodeids: Uint8Array,
+     *    subtrees: Array<Promise<SearchTree>|null>,
+     * }}
+     */
+    class SearchTreeBranchesArray extends SearchTreeBranches {
+        /**
+         * @param {Uint8Array} keys
+         * @param {Uint8Array} nodeids
+         */
+        constructor(keys, nodeids) {
+            super(keys.length, nodeids);
+            this.keys = keys;
+            let i = 1;
+            while (i < this.keys.length) {
+                if (this.keys[i - 1] >= this.keys[i]) {
+                    throw new Error("HERE");
+                }
+                i += 1;
+            }
+        }
+        /** @returns {Generator<[number, Promise<SearchTree>|null]>} */
+        * entries() {
+            let i = 0;
+            const l = this.keys.length;
+            while (i < l) {
+                yield [this.keys[i], this.subtrees[i]];
+                i += 1;
+            }
+        }
+        /**
+         * @param {number} k
+         * @returns {number}
+         */
+        getIndex(k) {
+            // Since length can't be bigger than 256,
+            // left + right can't overflow.
+            let left = 0;
+            let right = this.keys.length - 1;
+            while (left <= right) {
+                const mid = (left + right) >> 1;
+                if (this.keys[mid] < k) {
+                    left = mid + 1;
+                } else if (this.keys[mid] > k) {
+                    right = mid - 1;
+                } else {
+                    return mid;
+                }
+            }
+            return -1;
+        }
+        /**
+         * @param {number} i
+         * @returns {number}
+         */
+        getKey(i) {
+            return this.keys[i];
+        }
+        /**
+         * @returns {Uint8Array}
+         */
+        getKeys() {
+            return this.keys;
+        }
+    }
+
+    const EMPTY_SEARCH_TREE_BRANCHES = new SearchTreeBranchesArray(
+        EMPTY_UINT8,
+        EMPTY_UINT8,
+    );
+
+    /** @type {number[]} */
+    const SHORT_ALPHABITMAP_CHARS = [];
+    for (let i = 0x61; i <= 0x7A; ++i) {
+        if (i === 0x76 || i === 0x71) {
+            // 24 entries, 26 letters, so we skip q and v
+            continue;
+        }
+        SHORT_ALPHABITMAP_CHARS.push(i);
+    }
+
+    /** @type {number[]} */
+    const LONG_ALPHABITMAP_CHARS = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36];
+    for (let i = 0x61; i <= 0x7A; ++i) {
+        LONG_ALPHABITMAP_CHARS.push(i);
+    }
+
+    /**
+     * @param {number[]} alphabitmap_chars
+     * @param {number} width
+     * @return {(typeof SearchTreeBranches)&{"ALPHABITMAP_CHARS": number[], "width": number}}
+     */
+    function makeSearchTreeBranchesAlphaBitmapClass(alphabitmap_chars, width) {
+        const bitwidth = width * 8;
+        const cls = class SearchTreeBranchesAlphaBitmap extends SearchTreeBranches {
+            /**
+             * @param {number} bitmap
+             * @param {Uint8Array} nodeids
+             */
+            constructor(bitmap, nodeids) {
+                super(nodeids.length / 6, nodeids);
+                if (nodeids.length / 6 !== bitCount(bitmap)) {
+                    throw new Error(`mismatch ${bitmap} ${nodeids}`);
+                }
+                this.bitmap = bitmap;
+                this.nodeids = nodeids;
+            }
+            /** @returns {Generator<[number, Promise<SearchTree>|null]>} */
+            * entries() {
+                let i = 0;
+                let j = 0;
+                while (i < bitwidth) {
+                    if (this.bitmap & (1 << i)) {
+                        yield [alphabitmap_chars[i], this.subtrees[j]];
+                        j += 1;
+                    }
+                    i += 1;
+                }
+            }
+            /**
+             * @param {number} k
+             * @returns {number}
+             */
+            getIndex(k) {
+                //return this.getKeys().indexOf(k);
+                const ix = alphabitmap_chars.indexOf(k);
+                if (ix < 0) {
+                    return ix;
+                }
+                const result = bitCount(~(0xffffffff << ix) & this.bitmap);
+                return result >= this.subtrees.length ? -1 : result;
+            }
+            /**
+             * @param {number} branch_index
+             * @returns {number}
+             */
+            getKey(branch_index) {
+                //return this.getKeys()[branch_index];
+                let alpha_index = 0;
+                while (branch_index >= 0) {
+                    if (this.bitmap & (1 << alpha_index)) {
+                        branch_index -= 1;
+                    }
+                    alpha_index += 1;
+                }
+                return alphabitmap_chars[alpha_index];
+            }
+            /**
+             * @returns {Uint8Array}
+             */
+            getKeys() {
+                const length = bitCount(this.bitmap);
+                const result = new Uint8Array(length);
+                let result_index = 0;
+                for (let alpha_index = 0; alpha_index < bitwidth; ++alpha_index) {
+                    if (this.bitmap & (1 << alpha_index)) {
+                        result[result_index] = alphabitmap_chars[alpha_index];
+                        result_index += 1;
+                    }
+                }
+                return result;
+            }
+        };
+        cls.ALPHABITMAP_CHARS = alphabitmap_chars;
+        cls.width = width;
+        return cls;
+    }
+
+    const SearchTreeBranchesShortAlphaBitmap =
+        makeSearchTreeBranchesAlphaBitmapClass(SHORT_ALPHABITMAP_CHARS, 3);
+
+    const SearchTreeBranchesLongAlphaBitmap =
+        makeSearchTreeBranchesAlphaBitmapClass(LONG_ALPHABITMAP_CHARS, 4);
+
+    /**
+     * A [suffix tree], used for name-based search.
+     *
+     * This data structure is used to drive substring matches,
+     * such as matching the query "link" to `LinkedList`,
+     * and Lev-distance matches, such as matching the
+     * query "hahsmap" to `HashMap`.
+     *
+     * [suffix tree]: https://en.wikipedia.org/wiki/Suffix_tree
+     *
+     * branches
+     * : A sorted-array map of subtrees.
+     *
+     * data
+     * : The substring represented by this node. The root node
+     *   is always empty.
+     *
+     * leaves_suffix
+     * : The IDs of every entry that matches. Levenshtein matches
+     *   won't include these.
+     *
+     * leaves_whole
+     * : The IDs of every entry that matches exactly. Levenshtein matches
+     *   will include these.
+     *
+     * @type {{
+     *     might_have_prefix_branches: SearchTreeBranches,
+     *     branches: SearchTreeBranches,
+     *     data: Uint8Array,
+     *     leaves_suffix: RoaringBitmap,
+     *     leaves_whole: RoaringBitmap,
+     * }}
+     */
+    class SearchTree {
+        /**
+         * @param {SearchTreeBranches} branches
+         * @param {SearchTreeBranches} might_have_prefix_branches
+         * @param {Uint8Array} data
+         * @param {RoaringBitmap} leaves_whole
+         * @param {RoaringBitmap} leaves_suffix
+         */
+        constructor(
+            branches,
+            might_have_prefix_branches,
+            data,
+            leaves_whole,
+            leaves_suffix,
+        ) {
+            this.might_have_prefix_branches = might_have_prefix_branches;
+            this.branches = branches;
+            this.data = data;
+            this.leaves_suffix = leaves_suffix;
+            this.leaves_whole = leaves_whole;
+        }
+        /**
+         * Returns the Trie for the root node.
+         *
+         * A Trie pointer refers to a single node in a logical decompressed search tree
+         * (the real search tree is compressed).
+         *
+         * @return {Trie}
+         */
+        trie() {
+            return new Trie(this, 0);
+        }
+
+        /**
+         * Return the trie representing `name`
+         * @param {Uint8Array|string} name
+         * @returns {Promise<Trie?>}
+         */
+        async search(name) {
+            if (typeof name === "string") {
+                const utf8encoder = new TextEncoder();
+                name = utf8encoder.encode(name);
+            }
+            let trie = this.trie();
+            for (const datum of name) {
+                // code point definitely exists
+                const newTrie = trie.child(datum);
+                if (newTrie) {
+                    trie = await newTrie;
+                } else {
+                    return null;
+                }
+            }
+            return trie;
+        }
+
+        /**
+         * @param {Uint8Array|string} name
+         * @returns {AsyncGenerator<Trie>}
+         */
+        async* searchLev(name) {
+            if (typeof name === "string") {
+                const utf8encoder = new TextEncoder();
+                name = utf8encoder.encode(name);
+            }
+            const w = name.length;
+            if (w < 3) {
+                const trie = await this.search(name);
+                if (trie !== null) {
+                    yield trie;
+                }
+                return;
+            }
+            const levParams = w >= 6 ?
+                new Lev2TParametricDescription(w) :
+                new Lev1TParametricDescription(w);
+            /** @type {Array<[Promise<Trie>, number]>} */
+            const stack = [[Promise.resolve(this.trie()), 0]];
+            const n = levParams.n;
+            while (stack.length !== 0) {
+                // It's not empty
+                /** @type {[Promise<Trie>, number]} */
+                //@ts-expect-error
+                const [triePromise, levState] = stack.pop();
+                const trie = await triePromise;
+                for (const byte of trie.keysExcludeSuffixOnly()) {
+                    const levPos = levParams.getPosition(levState);
+                    const vector = levParams.getVector(
+                        name,
+                        byte,
+                        levPos,
+                        Math.min(w, levPos + (2 * n) + 1),
+                    );
+                    const newLevState = levParams.transition(
+                        levState,
+                        levPos,
+                        vector,
+                    );
+                    if (newLevState >= 0) {
+                        const child = trie.child(byte);
+                        if (child) {
+                            stack.push([child, newLevState]);
+                            if (levParams.isAccept(newLevState)) {
+                                yield child;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * A representation of a set of strings in the search index,
+     * as a subset of the entire tree.
+     */
+    class Trie {
+        /**
+         * @param {SearchTree} tree
+         * @param {number} offset
+         */
+        constructor(tree, offset) {
+            this.tree = tree;
+            this.offset = offset;
+        }
+
+        /**
+         * All exact matches for the string represented by this node.
+         * @returns {RoaringBitmap}
+         */
+        matches() {
+            if (this.offset === this.tree.data.length) {
+                return this.tree.leaves_whole;
+            } else {
+                return EMPTY_BITMAP;
+            }
+        }
+
+        /**
+         * All matches for strings that contain the string represented by this node.
+         * @returns {AsyncGenerator<RoaringBitmap>}
+         */
+        async* substringMatches() {
+            /** @type {Promise<SearchTree>[]} */
+            let layer = [Promise.resolve(this.tree)];
+            while (layer.length) {
+                const current_layer = layer;
+                layer = [];
+                for await (const tree of current_layer) {
+                    yield tree.leaves_whole.union(tree.leaves_suffix);
+                }
+                /** @type {HashTable<[number, SearchTree][]>} */
+                const subnodes = new HashTable();
+                for await (const node of current_layer) {
+                    const branches = node.branches;
+                    const l = branches.subtrees.length;
+                    for (let i = 0; i < l; ++i) {
+                        const subtree = branches.subtrees[i];
+                        if (subtree) {
+                            layer.push(subtree);
+                        } else if (subtree === null) {
+                            const byte = branches.getKey(i);
+                            const newnode = branches.getNodeID(i);
+                            if (!newnode) {
+                                throw new Error(`malformed tree; no node for key ${byte}`);
+                            } else {
+                                let subnode_list = subnodes.get(newnode);
+                                if (!subnode_list) {
+                                    subnode_list = [[byte, node]];
+                                    subnodes.set(newnode, subnode_list);
+                                } else {
+                                    subnode_list.push([byte, node]);
+                                }
+                            }
+                        } else {
+                            throw new Error(`malformed tree; index ${i} does not exist`);
+                        }
+                    }
+                }
+                for (const [newnode, subnode_list] of subnodes.entries()) {
+                    const res = registry.searchTreeLoadByNodeID(newnode);
+                    for (const [byte, node] of subnode_list) {
+                        const branches = node.branches;
+                        const might_have_prefix_branches = node.might_have_prefix_branches;
+                        const i = branches.getIndex(byte);
+                        branches.subtrees[i] = res;
+                        const mhpI = might_have_prefix_branches.getIndex(byte);
+                        if (mhpI !== -1) {
+                            might_have_prefix_branches.subtrees[mhpI] = res;
+                        }
+                    }
+                    layer.push(res);
+                }
+            }
+        }
+
+        /**
+         * All matches for strings that start with the string represented by this node.
+         * @returns {AsyncGenerator<RoaringBitmap>}
+         */
+        async* prefixMatches() {
+            /** @type {{node: Promise<SearchTree>, len: number}[]} */
+            let layer = [{node: Promise.resolve(this.tree), len: 0}];
+            // https://en.wikipedia.org/wiki/Heap_(data_structure)#Implementation_using_arrays
+            /** @type {{bitmap: RoaringBitmap, length: number}[]} */
+            const backlog = [];
+            while (layer.length !== 0 || backlog.length !== 0) {
+                const current_layer = layer;
+                layer = [];
+                let minLength = null;
+                // push every entry in the current layer into the backlog,
+                // a min-heap of result entries
+                // we then yield the smallest ones (can't yield bigger ones
+                // if we want to do them in order)
+                for (const {node, len} of current_layer) {
+                    const tree = await node;
+                    const length = len + tree.data.length;
+                    if (minLength === null || length < minLength) {
+                        minLength = length;
+                    }
+                    let backlogSlot = backlog.length;
+                    backlog.push({bitmap: tree.leaves_whole, length});
+                    while (backlogSlot > 0 &&
+                        backlog[backlogSlot].length < backlog[(backlogSlot - 1) >> 1].length
+                    ) {
+                        const parentSlot = (backlogSlot - 1) >> 1;
+                        const parent = backlog[parentSlot];
+                        backlog[parentSlot] = backlog[backlogSlot];
+                        backlog[backlogSlot] = parent;
+                        backlogSlot = parentSlot;
+                    }
+                }
+                // yield nodes in length order, smallest one first
+                // we know that, whatever the smallest item is
+                // every child will be bigger than that
+                while (backlog.length !== 0) {
+                    const backlogEntry = backlog[0];
+                    if (minLength !== null && backlogEntry.length > minLength) {
+                        break;
+                    }
+                    if (!backlogEntry.bitmap.isEmpty()) {
+                        yield backlogEntry.bitmap;
+                    }
+                    backlog[0] = backlog[backlog.length - 1];
+                    backlog.length -= 1;
+                    let backlogSlot = 0;
+                    const backlogLength = backlog.length;
+                    while (backlogSlot < backlogLength) {
+                        const leftSlot = (backlogSlot << 1) + 1;
+                        const rightSlot = (backlogSlot << 1) + 2;
+                        let smallest = backlogSlot;
+                        if (leftSlot < backlogLength &&
+                            backlog[leftSlot].length < backlog[smallest].length
+                        ) {
+                            smallest = leftSlot;
+                        }
+                        if (rightSlot < backlogLength &&
+                            backlog[rightSlot].length < backlog[smallest].length
+                        ) {
+                            smallest = rightSlot;
+                        }
+                        if (smallest === backlogSlot) {
+                            break;
+                        } else {
+                            const tmp = backlog[backlogSlot];
+                            backlog[backlogSlot] = backlog[smallest];
+                            backlog[smallest] = tmp;
+                            backlogSlot = smallest;
+                        }
+                    }
+                }
+                // if we still have more subtrees to walk, then keep going
+                /** @type {HashTable<{byte: number, tree: SearchTree, len: number}[]>} */
+                const subnodes = new HashTable();
+                for await (const {node, len} of current_layer) {
+                    const tree = await node;
+                    const length = len + tree.data.length;
+                    const mhp_branches = tree.might_have_prefix_branches;
+                    const l = mhp_branches.subtrees.length;
+                    for (let i = 0; i < l; ++i) {
+                        const len = length + 1;
+                        const subtree = mhp_branches.subtrees[i];
+                        if (subtree) {
+                            layer.push({node: subtree, len});
+                        } else if (subtree === null) {
+                            const byte = mhp_branches.getKey(i);
+                            const newnode = mhp_branches.getNodeID(i);
+                            if (!newnode) {
+                                throw new Error(`malformed tree; no node for key ${byte}`);
+                            } else {
+                                let subnode_list = subnodes.get(newnode);
+                                if (!subnode_list) {
+                                    subnode_list = [{byte, tree, len}];
+                                    subnodes.set(newnode, subnode_list);
+                                } else {
+                                    subnode_list.push({byte, tree, len});
+                                }
+                            }
+                        } else {
+                            throw new Error(`malformed tree; index ${i} does not exist`);
+                        }
+                    }
+                }
+                for (const [newnode, subnode_list] of subnodes.entries()) {
+                    const res = registry.searchTreeLoadByNodeID(newnode);
+                    let len = Number.MAX_SAFE_INTEGER;
+                    for (const {byte, tree, len: subtreelen} of subnode_list) {
+                        if (subtreelen < len) {
+                            len = subtreelen;
+                        }
+                        const mhp_branches = tree.might_have_prefix_branches;
+                        const i = mhp_branches.getIndex(byte);
+                        mhp_branches.subtrees[i] = res;
+                        const branches = tree.branches;
+                        const bi = branches.getIndex(byte);
+                        branches.subtrees[bi] = res;
+                    }
+                    layer.push({node: res, len});
+                }
+            }
+        }
+
+        /**
+         * Returns all keys that are children of this node.
+         * @returns {Uint8Array}
+         */
+        keys() {
+            const data = this.tree.data;
+            if (this.offset === data.length) {
+                return this.tree.branches.getKeys();
+            } else {
+                return Uint8Array.of(data[this.offset]);
+            }
+        }
+
+        /**
+         * Returns all nodes that are direct children of this node.
+         * @returns {[number, Promise<Trie>][]}
+         */
+        children() {
+            const data = this.tree.data;
+            if (this.offset === data.length) {
+                /** @type {[number, Promise<Trie>][]} */
+                const nodes = [];
+                let i = 0;
+                for (const [k, v] of this.tree.branches.entries()) {
+                    /** @type {Promise<SearchTree>} */
+                    let node;
+                    if (v) {
+                        node = v;
+                    } else {
+                        const newnode = this.tree.branches.getNodeID(i);
+                        if (!newnode) {
+                            throw new Error(`malformed tree; no hash for key ${k}: ${newnode} \
+                                ${this.tree.branches.nodeids} ${this.tree.branches.getKeys()}`);
+                        }
+                        node = registry.searchTreeLoadByNodeID(newnode);
+                        this.tree.branches.subtrees[i] = node;
+                        const mhpI = this.tree.might_have_prefix_branches.getIndex(k);
+                        if (mhpI !== -1) {
+                            this.tree.might_have_prefix_branches.subtrees[mhpI] = node;
+                        }
+                    }
+                    nodes.push([k, node.then(node => node.trie())]);
+                    i += 1;
+                }
+                return nodes;
+            } else {
+                /** @type {number} */
+                const codePoint = data[this.offset];
+                const trie = new Trie(this.tree, this.offset + 1);
+                return [[codePoint, Promise.resolve(trie)]];
+            }
+        }
+
+        /**
+         * Returns all keys that are children of this node.
+         * @returns {Uint8Array}
+         */
+        keysExcludeSuffixOnly() {
+            const data = this.tree.data;
+            if (this.offset === data.length) {
+                return this.tree.might_have_prefix_branches.getKeys();
+            } else {
+                return Uint8Array.of(data[this.offset]);
+            }
+        }
+
+        /**
+         * Returns all nodes that are direct children of this node.
+         * @returns {[number, Promise<Trie>][]}
+         */
+        childrenExcludeSuffixOnly() {
+            const data = this.tree.data;
+            if (this.offset === data.length) {
+                /** @type {[number, Promise<Trie>][]} */
+                const nodes = [];
+                let i = 0;
+                for (const [k, v] of this.tree.might_have_prefix_branches.entries()) {
+                    /** @type {Promise<SearchTree>} */
+                    let node;
+                    if (v) {
+                        node = v;
+                    } else {
+                        const newnode = this.tree.might_have_prefix_branches.getNodeID(i);
+                        if (!newnode) {
+                            throw new Error(`malformed tree; no node for key ${k}`);
+                        }
+                        node = registry.searchTreeLoadByNodeID(newnode);
+                        this.tree.might_have_prefix_branches.subtrees[i] = node;
+                        this.tree.branches.subtrees[this.tree.branches.getIndex(k)] = node;
+                    }
+                    nodes.push([k, node.then(node => node.trie())]);
+                    i += 1;
+                }
+                return nodes;
+            } else {
+                /** @type {number} */
+                const codePoint = data[this.offset];
+                const trie = new Trie(this.tree, this.offset + 1);
+                return [[codePoint, Promise.resolve(trie)]];
+            }
+        }
+
+        /**
+         * Returns a single node that is a direct child of this node.
+         * @param {number} byte
+         * @returns {Promise<Trie>?}
+         */
+        child(byte) {
+            if (this.offset === this.tree.data.length) {
+                const i = this.tree.branches.getIndex(byte);
+                if (i !== -1) {
+                    let branch = this.tree.branches.subtrees[i];
+                    if (branch === null) {
+                        const newnode = this.tree.branches.getNodeID(i);
+                        if (!newnode) {
+                            throw new Error(`malformed tree; no node for key ${byte}`);
+                        }
+                        branch = registry.searchTreeLoadByNodeID(newnode);
+                        this.tree.branches.subtrees[i] = branch;
+                        const mhpI = this.tree.might_have_prefix_branches.getIndex(byte);
+                        if (mhpI !== -1) {
+                            this.tree.might_have_prefix_branches.subtrees[mhpI] = branch;
+                        }
+                    }
+                    return branch.then(branch => branch.trie());
+                }
+            } else if (this.tree.data[this.offset] === byte) {
+                return Promise.resolve(new Trie(this.tree, this.offset + 1));
+            }
+            return null;
+        }
+    }
+
+    class DataColumn {
+        /**
+         * Construct the wrapper object for a data column.
+         * @param {number[]} counts
+         * @param {Uint8Array} hashes
+         * @param {RoaringBitmap} emptyset
+         * @param {string} name
+         */
+        constructor(counts, hashes, emptyset, name) {
+            this.hashes = hashes;
+            this.emptyset = emptyset;
+            this.name = name;
+            /** @type {{"hash": Uint8Array, "data": Promise<Uint8Array[]>?, "end": number}[]} */
+            this.buckets = [];
+            this.bucket_keys = [];
+            const l = counts.length;
+            let k = 0;
+            let totalLength = 0;
+            for (let i = 0; i < l; ++i) {
+                const count = counts[i];
+                totalLength += count;
+                const start = k;
+                for (let j = 0; j < count; ++j) {
+                    if (emptyset.contains(k)) {
+                        j -= 1;
+                    }
+                    k += 1;
+                }
+                const end = k;
+                const bucket = {hash: hashes.subarray(i * 6, (i + 1) * 6), data: null, end, count};
+                this.buckets.push(bucket);
+                this.bucket_keys.push(start);
+            }
+            this.length = totalLength;
+        }
+        /**
+         * Check if a cell contains the empty string.
+         * @param {number} id
+         * @returns {boolean}
+         */
+        isEmpty(id) {
+            return this.emptyset.contains(id);
+        }
+        /**
+         * Look up a cell by row ID.
+         * @param {number} id
+         * @returns {Promise<Uint8Array|undefined>}
+         */
+        async at(id) {
+            if (this.emptyset.contains(id)) {
+                return Promise.resolve(EMPTY_UINT8);
+            } else {
+                let idx = -1;
+                while (this.bucket_keys[idx + 1] <= id) {
+                    idx += 1;
+                }
+                if (idx === -1 || idx >= this.bucket_keys.length) {
+                    return Promise.resolve(undefined);
+                } else {
+                    const start = this.bucket_keys[idx];
+                    const {hash, end} = this.buckets[idx];
+                    let data = this.buckets[idx].data;
+                    if (data === null) {
+                        const dataSansEmptyset = await registry.dataLoadByNameAndHash(
+                            this.name,
+                            hash,
+                        );
+                        // After the `await` resolves, another task might fill
+                        // in the data. If so, we should use that.
+                        data = this.buckets[idx].data;
+                        if (data !== null) {
+                            return (await data)[id - start];
+                        }
+                        /** @type {(Uint8Array[])|null} */
+                        let dataWithEmptyset = null;
+                        let pos = start;
+                        let insertCount = 0;
+                        while (pos < end) {
+                            if (this.emptyset.contains(pos)) {
+                                if (dataWithEmptyset === null) {
+                                    dataWithEmptyset = dataSansEmptyset.splice(0, insertCount);
+                                } else if (insertCount !== 0) {
+                                    dataWithEmptyset.push(
+                                        ...dataSansEmptyset.splice(0, insertCount),
+                                    );
+                                }
+                                insertCount = 0;
+                                dataWithEmptyset.push(EMPTY_UINT8);
+                            } else {
+                                insertCount += 1;
+                            }
+                            pos += 1;
+                        }
+                        data = Promise.resolve(
+                            dataWithEmptyset === null ?
+                                dataSansEmptyset :
+                                dataWithEmptyset.concat(dataSansEmptyset),
+                        );
+                        this.buckets[idx].data = data;
+                    }
+                    return (await data)[id - start];
+                }
+            }
+        }
+    }
+
+    class Database {
+        /**
+         * The primary frontend for accessing data in this index.
+         *
+         * @param {Map<string, SearchTree>} searchTreeRoots
+         * @param {Map<string, DataColumn>} dataColumns
+         */
+        constructor(searchTreeRoots, dataColumns) {
+            this.searchTreeRoots = searchTreeRoots;
+            this.dataColumns = dataColumns;
+        }
+        /**
+         * Search a column by name, returning verbatim matched IDs.
+         * @param {string} colname
+         * @returns {SearchTree|undefined}
+         */
+        getIndex(colname) {
+            return this.searchTreeRoots.get(colname);
+        }
+        /**
+         * Look up a cell by column ID and row ID.
+         * @param {string} colname
+         * @returns {DataColumn|undefined}
+         */
+        getData(colname) {
+            return this.dataColumns.get(colname);
+        }
+    }
+
+    /**
+     * Load a data column.
+     * @param {Uint8Array} data
+     */
+    function loadColumnFromBytes(data) {
+        const hashBuf = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0);
+        const truncatedHash = hashBuf.subarray(2, 8);
+        siphashOfBytes(data, 0, 0, 0, 0, hashBuf);
+        const cb = registry.dataColumnLoadPromiseCallbacks.get(truncatedHash);
+        if (cb) {
+            const backrefs = [];
+            const dataSansEmptyset = [];
+            let i = 0;
+            const l = data.length;
+            while (i < l) {
+                let c = data[i];
+                if (c >= 48 && c <= 63) { // 48 = "0", 63 = "?"
+                    dataSansEmptyset.push(backrefs[c - 48]);
+                    i += 1;
+                } else {
+                    let n = 0;
+                    while (c < 96) { // 96 = "`"
+                        n = (n << 4) | (c & 0xF);
+                        i += 1;
+                        c = data[i];
+                    }
+                    n = (n << 4) | (c & 0xF);
+                    i += 1;
+                    const item = data.subarray(i, i + n);
+                    dataSansEmptyset.push(item);
+                    i += n;
+                    backrefs.unshift(item);
+                    if (backrefs.length > 16) {
+                        backrefs.pop();
+                    }
+                }
+            }
+            cb(null, dataSansEmptyset);
+        }
+    }
+
+    /**
+     * @param {string} inputBase64
+     * @returns {[Uint8Array, SearchTree]}
+     */
+    function makeSearchTreeFromBase64(inputBase64) {
+        const input = makeUint8ArrayFromBase64(inputBase64);
+        let i = 0;
+        const l = input.length;
+        /** @type {HashTable<SearchTree>} */
+        const stash = new HashTable();
+        const hash = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0);
+        const truncatedHash = new Uint8Array(hash.buffer, 2, 6);
+        // used for handling compressed (that is, relative-offset) nodes
+        /** @type {{hash: Uint8Array, used: boolean}[]} */
+        const hash_history = [];
+        /** @type {Uint8Array[]} */
+        const data_history = [];
+        let canonical = EMPTY_UINT8;
+        /** @type {SearchTree} */
+        let tree = new SearchTree(
+            EMPTY_SEARCH_TREE_BRANCHES,
+            EMPTY_SEARCH_TREE_BRANCHES,
+            EMPTY_UINT8,
+            EMPTY_BITMAP,
+            EMPTY_BITMAP,
+        );
+        /**
+         * @param {Uint8Array} input
+         * @param {number} i
+         * @param {number} compression_tag
+         * @returns {{
+         *     "cpbranches": Uint8Array,
+         *     "csbranches": Uint8Array,
+         *     "might_have_prefix_branches": SearchTreeBranches,
+         *     "branches": SearchTreeBranches,
+         *     "cpnodes": Uint8Array,
+         *     "csnodes": Uint8Array,
+         *     "consumed_len_bytes": number,
+         * }}
+         */
+        function makeBranchesFromBinaryData(
+            input,
+            i,
+            compression_tag,
+        ) {
+            const is_pure_suffixes_only_node = (compression_tag & 0x01) !== 0x00;
+            const is_stack_compressed = (compression_tag & 0x02) !== 0;
+            const is_long_compressed = (compression_tag & 0x04) !== 0;
+            const all_children_are_compressed =
+                (compression_tag & 0xF0) === 0xF0 && !is_long_compressed;
+            const any_children_are_compressed =
+                (compression_tag & 0xF0) !== 0x00 || is_long_compressed;
+            const start_point = i;
+            let cplen;
+            let cslen;
+            let alphabitmap = null;
+            if (is_pure_suffixes_only_node) {
+                cplen = 0;
+                cslen = input[i];
+                i += 1;
+                if (cslen >= 0xc0) {
+                    alphabitmap = SearchTreeBranchesLongAlphaBitmap;
+                    cslen = cslen & 0x3F;
+                } else if (cslen >= 0x80) {
+                    alphabitmap = SearchTreeBranchesShortAlphaBitmap;
+                    cslen = cslen & 0x7F;
+                }
+            } else {
+                cplen = input[i];
+                i += 1;
+                cslen = input[i];
+                i += 1;
+                if (cplen === 0xff && cslen === 0xff) {
+                    cplen = 0x100;
+                    cslen = 0;
+                } else if (cplen >= 0xc0 && cslen >= 0xc0) {
+                    alphabitmap = SearchTreeBranchesLongAlphaBitmap;
+                    cplen = cplen & 0x3F;
+                    cslen = cslen & 0x3F;
+                } else if (cplen >= 0x80 && cslen >= 0x80) {
+                    alphabitmap = SearchTreeBranchesShortAlphaBitmap;
+                    cplen = cplen & 0x7F;
+                    cslen = cslen & 0x7F;
+                }
+            }
+            let j = 0;
+            /** @type {Uint8Array} */
+            let cpnodes;
+            if (any_children_are_compressed) {
+                cpnodes = cplen === 0 ? EMPTY_UINT8 : new Uint8Array(cplen * 6);
+                while (j < cplen) {
+                    const is_compressed = all_children_are_compressed ||
+                        ((0x10 << j) & compression_tag) !== 0;
+                    if (is_compressed) {
+                        let slot = hash_history.length - 1;
+                        if (is_stack_compressed) {
+                            while (hash_history[slot].used) {
+                                slot -= 1;
+                            }
+                        } else {
+                            slot -= input[i];
+                            i += 1;
+                        }
+                        hash_history[slot].used = true;
+                        cpnodes.set(
+                            hash_history[slot].hash,
+                            j * 6,
+                        );
+                    } else {
+                        const joff = j * 6;
+                        cpnodes[joff + 0] = input[i + 0];
+                        cpnodes[joff + 1] = input[i + 1];
+                        cpnodes[joff + 2] = input[i + 2];
+                        cpnodes[joff + 3] = input[i + 3];
+                        cpnodes[joff + 4] = input[i + 4];
+                        cpnodes[joff + 5] = input[i + 5];
+                        i += 6;
+                    }
+                    j += 1;
+                }
+            } else {
+                cpnodes = cplen === 0 ? EMPTY_UINT8 : input.subarray(i, i + (cplen * 6));
+                i += cplen * 6;
+            }
+            j = 0;
+            /** @type {Uint8Array} */
+            let csnodes;
+            if (any_children_are_compressed) {
+                csnodes = cslen === 0 ? EMPTY_UINT8 : new Uint8Array(cslen * 6);
+                while (j < cslen) {
+                    const is_compressed = all_children_are_compressed ||
+                        ((0x10 << (cplen + j)) & compression_tag) !== 0;
+                    if (is_compressed) {
+                        let slot = hash_history.length - 1;
+                        if (is_stack_compressed) {
+                            while (hash_history[slot].used) {
+                                slot -= 1;
+                            }
+                        } else {
+                            slot -= input[i];
+                            i += 1;
+                        }
+                        hash_history[slot].used = true;
+                        csnodes.set(
+                            hash_history[slot].hash,
+                            j * 6,
+                        );
+                    } else {
+                        const joff = j * 6;
+                        csnodes[joff + 0] = input[i + 0];
+                        csnodes[joff + 1] = input[i + 1];
+                        csnodes[joff + 2] = input[i + 2];
+                        csnodes[joff + 3] = input[i + 3];
+                        csnodes[joff + 4] = input[i + 4];
+                        csnodes[joff + 5] = input[i + 5];
+                        i += 6;
+                    }
+                    j += 1;
+                }
+            } else {
+                csnodes = cslen === 0 ? EMPTY_UINT8 : input.subarray(i, i + (cslen * 6));
+                i += cslen * 6;
+            }
+            let cpbranches;
+            let might_have_prefix_branches;
+            if (cplen === 0) {
+                cpbranches = EMPTY_UINT8;
+                might_have_prefix_branches = EMPTY_SEARCH_TREE_BRANCHES;
+            } else if (alphabitmap) {
+                cpbranches = new Uint8Array(input.buffer, i + input.byteOffset, alphabitmap.width);
+                const branchset = (alphabitmap.width === 4 ? (input[i + 3] << 24) : 0) |
+                    (input[i + 2] << 16) |
+                    (input[i + 1] << 8) |
+                    input[i];
+                might_have_prefix_branches = new alphabitmap(branchset, cpnodes);
+                i += alphabitmap.width;
+            } else {
+                cpbranches = new Uint8Array(input.buffer, i + input.byteOffset, cplen);
+                might_have_prefix_branches = new SearchTreeBranchesArray(cpbranches, cpnodes);
+                i += cplen;
+            }
+            let csbranches;
+            let branches;
+            if (cslen === 0) {
+                csbranches = EMPTY_UINT8;
+                branches = might_have_prefix_branches;
+            } else if (alphabitmap) {
+                csbranches = new Uint8Array(input.buffer, i + input.byteOffset, alphabitmap.width);
+                const branchset = (alphabitmap.width === 4 ? (input[i + 3] << 24) : 0) |
+                    (input[i + 2] << 16) |
+                    (input[i + 1] << 8) |
+                    input[i];
+                if (cplen === 0) {
+                    branches = new alphabitmap(branchset, csnodes);
+                } else {
+                    const cpoffset = i - alphabitmap.width;
+                    const cpbranchset =
+                        (alphabitmap.width === 4 ? (input[cpoffset + 3] << 24) : 0) |
+                        (input[cpoffset + 2] << 16) |
+                        (input[cpoffset + 1] << 8) |
+                        input[cpoffset];
+                    const hashes = new Uint8Array((cplen + cslen) * 6);
+                    let cpi = 0;
+                    let csi = 0;
+                    let j = 0;
+                    for (let k = 0; k < alphabitmap.ALPHABITMAP_CHARS.length; k += 1) {
+                        if (branchset & (1 << k)) {
+                            hashes[j + 0] = csnodes[csi + 0];
+                            hashes[j + 1] = csnodes[csi + 1];
+                            hashes[j + 2] = csnodes[csi + 2];
+                            hashes[j + 3] = csnodes[csi + 3];
+                            hashes[j + 4] = csnodes[csi + 4];
+                            hashes[j + 5] = csnodes[csi + 5];
+                            j += 6;
+                            csi += 6;
+                        } else if (cpbranchset & (1 << k)) {
+                            hashes[j + 0] = cpnodes[cpi + 0];
+                            hashes[j + 1] = cpnodes[cpi + 1];
+                            hashes[j + 2] = cpnodes[cpi + 2];
+                            hashes[j + 3] = cpnodes[cpi + 3];
+                            hashes[j + 4] = cpnodes[cpi + 4];
+                            hashes[j + 5] = cpnodes[cpi + 5];
+                            j += 6;
+                            cpi += 6;
+                        }
+                    }
+                    branches = new alphabitmap(branchset | cpbranchset, hashes);
+                }
+                i += alphabitmap.width;
+            } else {
+                csbranches = new Uint8Array(input.buffer, i + input.byteOffset, cslen);
+                if (cplen === 0) {
+                    branches = new SearchTreeBranchesArray(csbranches, csnodes);
+                } else {
+                    const branchset = new Uint8Array(cplen + cslen);
+                    const hashes = new Uint8Array((cplen + cslen) * 6);
+                    let cpi = 0;
+                    let csi = 0;
+                    let j = 0;
+                    while (cpi < cplen || csi < cslen) {
+                        if (cpi >= cplen || (csi < cslen && cpbranches[cpi] > csbranches[csi])) {
+                            branchset[j] = csbranches[csi];
+                            const joff = j * 6;
+                            const csioff = csi * 6;
+                            hashes[joff + 0] = csnodes[csioff + 0];
+                            hashes[joff + 1] = csnodes[csioff + 1];
+                            hashes[joff + 2] = csnodes[csioff + 2];
+                            hashes[joff + 3] = csnodes[csioff + 3];
+                            hashes[joff + 4] = csnodes[csioff + 4];
+                            hashes[joff + 5] = csnodes[csioff + 5];
+                            csi += 1;
+                        } else {
+                            branchset[j] = cpbranches[cpi];
+                            const joff = j * 6;
+                            const cpioff = cpi * 6;
+                            hashes[joff + 0] = cpnodes[cpioff + 0];
+                            hashes[joff + 1] = cpnodes[cpioff + 1];
+                            hashes[joff + 2] = cpnodes[cpioff + 2];
+                            hashes[joff + 3] = cpnodes[cpioff + 3];
+                            hashes[joff + 4] = cpnodes[cpioff + 4];
+                            hashes[joff + 5] = cpnodes[cpioff + 5];
+                            cpi += 1;
+                        }
+                        j += 1;
+                    }
+                    branches = new SearchTreeBranchesArray(branchset, hashes);
+                }
+                i += cslen;
+            }
+            return {
+                consumed_len_bytes: i - start_point,
+                cpbranches,
+                csbranches,
+                cpnodes,
+                csnodes,
+                branches,
+                might_have_prefix_branches,
+            };
+        }
+        while (i < l) {
+            const start = i;
+            let data;
+            // compression_tag = 1 means pure-suffixes-only,
+            // which is not considered "compressed" for the purposes of this loop
+            // because that's the canonical, hashed version of the data
+            let compression_tag = input[i];
+            const is_pure_suffixes_only_node = (compression_tag & 0x01) !== 0;
+            if (compression_tag > 1) {
+                // compressed node
+                const is_long_compressed = (compression_tag & 0x04) !== 0;
+                const is_data_compressed = (compression_tag & 0x08) !== 0;
+                i += 1;
+                if (is_long_compressed) {
+                    compression_tag |= input[i] << 8;
+                    i += 1;
+                    compression_tag |= input[i] << 16;
+                    i += 1;
+                }
+                let dlen = input[i];
+                i += 1;
+                if (is_data_compressed) {
+                    data = data_history[data_history.length - dlen - 1];
+                    dlen = data.length;
+                } else {
+                    data = dlen === 0 ?
+                        EMPTY_UINT8 :
+                        new Uint8Array(input.buffer, i + input.byteOffset, dlen);
+                    i += dlen;
+                }
+                const coffset = i;
+                const {
+                    cpbranches,
+                    csbranches,
+                    cpnodes,
+                    csnodes,
+                    consumed_len_bytes: branches_consumed_len_bytes,
+                    branches,
+                    might_have_prefix_branches,
+                } = makeBranchesFromBinaryData(input, i, compression_tag);
+                i += branches_consumed_len_bytes;
+                let whole;
+                let suffix;
+                if (is_pure_suffixes_only_node) {
+                    whole = EMPTY_BITMAP;
+                    suffix = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += suffix.consumed_len_bytes;
+                } else if (input[i] === 0xff) {
+                    whole = EMPTY_BITMAP;
+                    suffix = EMPTY_BITMAP1;
+                    i += 1;
+                } else {
+                    whole = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += whole.consumed_len_bytes;
+                    suffix = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += suffix.consumed_len_bytes;
+                }
+                tree = new SearchTree(
+                    branches,
+                    might_have_prefix_branches,
+                    data,
+                    whole,
+                    suffix,
+                );
+                const clen = (
+                    (is_pure_suffixes_only_node ? 3 : 4) + // lengths of children and data
+                    dlen +
+                    cpnodes.length + csnodes.length +
+                    cpbranches.length + csbranches.length +
+                    whole.consumed_len_bytes +
+                    suffix.consumed_len_bytes
+                );
+                if (canonical.length < clen) {
+                    canonical = new Uint8Array(clen);
+                }
+                let ci = 0;
+                canonical[ci] = is_pure_suffixes_only_node ? 1 : 0;
+                ci += 1;
+                canonical[ci] = dlen;
+                ci += 1;
+                canonical.set(data, ci);
+                ci += dlen;
+                canonical[ci] = input[coffset];
+                ci += 1;
+                if (!is_pure_suffixes_only_node) {
+                    canonical[ci] = input[coffset + 1];
+                    ci += 1;
+                }
+                canonical.set(cpnodes, ci);
+                ci += cpnodes.length;
+                canonical.set(csnodes, ci);
+                ci += csnodes.length;
+                canonical.set(cpbranches, ci);
+                ci += cpbranches.length;
+                canonical.set(csbranches, ci);
+                ci += csbranches.length;
+                const leavesOffset = i - whole.consumed_len_bytes - suffix.consumed_len_bytes;
+                for (let j = leavesOffset; j < i; j += 1) {
+                    canonical[ci + j - leavesOffset] = input[j];
+                }
+                siphashOfBytes(canonical.subarray(0, clen), 0, 0, 0, 0, hash);
+                hash[2] &= 0x7f;
+            } else {
+                // uncompressed node
+                const dlen = input [i + 1];
+                i += 2;
+                if (dlen === 0) {
+                    data = EMPTY_UINT8;
+                } else {
+                    data = new Uint8Array(input.buffer, i + input.byteOffset, dlen);
+                }
+                i += dlen;
+                const {
+                    consumed_len_bytes: branches_consumed_len_bytes,
+                    branches,
+                    might_have_prefix_branches,
+                } = makeBranchesFromBinaryData(input, i, compression_tag);
+                i += branches_consumed_len_bytes;
+                let whole;
+                let suffix;
+                if (is_pure_suffixes_only_node) {
+                    whole = EMPTY_BITMAP;
+                    suffix = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += suffix.consumed_len_bytes;
+                } else if (input[i] === 0xff) {
+                    whole = EMPTY_BITMAP;
+                    suffix = EMPTY_BITMAP;
+                    i += 1;
+                } else {
+                    whole = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += whole.consumed_len_bytes;
+                    suffix = input[i] === 0 ?
+                        EMPTY_BITMAP1 :
+                        new RoaringBitmap(input, i);
+                    i += suffix.consumed_len_bytes;
+                }
+                siphashOfBytes(new Uint8Array(
+                    input.buffer,
+                    start + input.byteOffset,
+                    i - start,
+                ), 0, 0, 0, 0, hash);
+                hash[2] &= 0x7f;
+                tree = new SearchTree(
+                    branches,
+                    might_have_prefix_branches,
+                    data,
+                    whole,
+                    suffix,
+                );
+            }
+            hash_history.push({hash: truncatedHash.slice(), used: false});
+            if (data.length !== 0) {
+                data_history.push(data);
+            }
+            const tree_branch_nodeids = tree.branches.nodeids;
+            const tree_branch_subtrees = tree.branches.subtrees;
+            let j = 0;
+            let lb = tree.branches.subtrees.length;
+            while (j < lb) {
+                // node id with a 1 in its most significant bit is inlined, and, so
+                // it won't be in the stash
+                if ((tree_branch_nodeids[j * 6] & 0x80) === 0) {
+                    const subtree = stash.getWithOffsetKey(tree_branch_nodeids, j * 6);
+                    if (subtree !== undefined) {
+                        tree_branch_subtrees[j] = Promise.resolve(subtree);
+                    }
+                }
+                j += 1;
+            }
+            const tree_mhp_branch_nodeids = tree.might_have_prefix_branches.nodeids;
+            const tree_mhp_branch_subtrees = tree.might_have_prefix_branches.subtrees;
+            j = 0;
+            lb = tree.might_have_prefix_branches.subtrees.length;
+            while (j < lb) {
+                // node id with a 1 in its most significant bit is inlined, and, so
+                // it won't be in the stash
+                if ((tree_mhp_branch_nodeids[j * 6] & 0x80) === 0) {
+                    const subtree = stash.getWithOffsetKey(tree_mhp_branch_nodeids, j * 6);
+                    if (subtree !== undefined) {
+                        tree_mhp_branch_subtrees[j] = Promise.resolve(subtree);
+                    }
+                }
+                j += 1;
+            }
+            if (i !== l) {
+                stash.set(truncatedHash, tree);
+            }
+        }
+        return [truncatedHash, tree];
+    }
+
+    return new Promise((resolve, reject) => {
+        registry.searchTreeRootCallback = (error, data) => {
+            if (data) {
+                resolve(data);
+            } else {
+                reject(error);
+            }
+        };
+        hooks.loadRoot(callbacks);
+    });
+}
+
+if (typeof window !== "undefined") {
+    window.Stringdex = {
+        loadDatabase,
+    };
+    window.RoaringBitmap = RoaringBitmap;
+    if (window.StringdexOnload) {
+        window.StringdexOnload.forEach(cb => cb(window.Stringdex));
+    }
+} else {
+    /** @type {stringdex.Stringdex} */
+    // eslint-disable-next-line no-undef
+    module.exports.Stringdex = {
+        loadDatabase,
+    };
+    /** @type {stringdex.RoaringBitmap} */
+    // eslint-disable-next-line no-undef
+    module.exports.RoaringBitmap = RoaringBitmap;
+}
+
+// eslint-disable-next-line max-len
+// polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/fromBase64
+/**
+ * @type {function(string): Uint8Array} base64
+ */
+//@ts-expect-error
+const makeUint8ArrayFromBase64 = Uint8Array.fromBase64 ? Uint8Array.fromBase64 : (string => {
+    const bytes_as_string = atob(string);
+    const l = bytes_as_string.length;
+    const bytes = new Uint8Array(l);
+    for (let i = 0; i < l; ++i) {
+        bytes[i] = bytes_as_string.charCodeAt(i);
+    }
+    return bytes;
+});
+/**
+ * @type {function(string): Uint8Array} base64
+ */
+//@ts-expect-error
+const makeUint8ArrayFromHex = Uint8Array.fromHex ? Uint8Array.fromHex : (string => {
+    /** @type {Object<string, number>} */
+    const alpha = {
+        "0": 0, "1": 1,
+        "2": 2, "3": 3,
+        "4": 4, "5": 5,
+        "6": 6, "7": 7,
+        "8": 8, "9": 9,
+        "a": 10, "b": 11,
+        "A": 10, "B": 11,
+        "c": 12, "d": 13,
+        "C": 12, "D": 13,
+        "e": 14, "f": 15,
+        "E": 14, "F": 15,
+    };
+    const l = string.length >> 1;
+    const bytes = new Uint8Array(l);
+    for (let i = 0; i < l; i += 1) {
+        const top = string[i << 1];
+        const bottom = string[(i << 1) + 1];
+        bytes[i] = (alpha[top] << 4) | alpha[bottom];
+    }
+    return bytes;
+});
+
+/**
+ * @type {function(Uint8Array): string} base64
+ */
+//@ts-expect-error
+const makeHexFromUint8Array = Uint8Array.prototype.toHex ? (array => array.toHex()) : (array => {
+    /** @type {string[]} */
+    const alpha = [
+        "0", "1",
+        "2", "3",
+        "4", "5",
+        "6", "7",
+        "8", "9",
+        "a", "b",
+        "c", "d",
+        "e", "f",
+    ];
+    const l = array.length;
+    const v = [];
+    for (let i = 0; i < l; ++i) {
+        v.push(alpha[array[i] >> 4]);
+        v.push(alpha[array[i] & 0xf]);
+    }
+    return v.join("");
+});
+
+//////////////
+
+/**
+ * SipHash 1-3
+ * @param {Uint8Array} input data to be hashed; all codepoints in string should be less than 256
+ * @param {number} k0lo first word of key
+ * @param {number} k0hi second word of key
+ * @param {number} k1lo third word of key
+ * @param {number} k1hi fourth word of key
+ * @param {Uint8Array} output the data to write (clobber the first eight bytes)
+ */
+function siphashOfBytes(input, k0lo, k0hi, k1lo, k1hi, output) {
+    // hash state
+    // While siphash uses 64 bit state, js only has native support
+    // for 32 bit numbers. BigInt, unfortunately, doesn't count.
+    // It's too slow.
+    let v0lo = k0lo ^ 0x70736575;
+    let v0hi = k0hi ^ 0x736f6d65;
+    let v1lo = k1lo ^ 0x6e646f6d;
+    let v1hi = k1hi ^ 0x646f7261;
+    let v2lo = k0lo ^ 0x6e657261;
+    let v2hi = k0hi ^ 0x6c796765;
+    let v3lo = k1lo ^ 0x79746573;
+    let v3hi = k1hi ^ 0x74656462;
+    const inputLength = input.length;
+    let inputI = 0;
+    // main hash loop
+    const left = inputLength & 0x7;
+    let milo = 0;
+    let mihi = 0;
+    while (inputI < inputLength - left) {
+        u8ToU64le(inputI, inputI + 8);
+        v3lo ^= milo;
+        v3hi ^= mihi;
+        siphashCompress();
+        v0lo ^= milo;
+        v0hi ^= mihi;
+        inputI += 8;
+    }
+    u8ToU64le(inputI, inputI + left);
+    // finish
+    const blo = milo;
+    const bhi = ((inputLength & 0xff) << 24) | mihi;
+    v3lo ^= blo;
+    v3hi ^= bhi;
+    siphashCompress();
+    v0lo ^= blo;
+    v0hi ^= bhi;
+    v2lo ^= 0xff;
+    siphashCompress();
+    siphashCompress();
+    siphashCompress();
+    output[7] = (v0lo ^ v1lo ^ v2lo ^ v3lo) & 0xff;
+    output[6] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 8;
+    output[5] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 16;
+    output[4] = (v0lo ^ v1lo ^ v2lo ^ v3lo) >>> 24;
+    output[3] = (v0hi ^ v1hi ^ v2hi ^ v3hi) & 0xff;
+    output[2] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 8;
+    output[1] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 16;
+    output[0] = (v0hi ^ v1hi ^ v2hi ^ v3hi) >>> 24;
+    /**
+     * Convert eight bytes to a single 64-bit number
+     * @param {number} offset
+     * @param {number} length
+     */
+    function u8ToU64le(offset, length) {
+        const n0 = offset < length ? input[offset] & 0xff : 0;
+        const n1 = offset + 1 < length ? input[offset + 1] & 0xff : 0;
+        const n2 = offset + 2 < length ? input[offset + 2] & 0xff : 0;
+        const n3 = offset + 3 < length ? input[offset + 3] & 0xff : 0;
+        const n4 = offset + 4 < length ? input[offset + 4] & 0xff : 0;
+        const n5 = offset + 5 < length ? input[offset + 5] & 0xff : 0;
+        const n6 = offset + 6 < length ? input[offset + 6] & 0xff : 0;
+        const n7 = offset + 7 < length ? input[offset + 7] & 0xff : 0;
+        milo = n0 | (n1 << 8) | (n2 << 16) | (n3 << 24);
+        mihi = n4 | (n5 << 8) | (n6 << 16) | (n7 << 24);
+    }
+    function siphashCompress() {
+        // v0 += v1;
+        v0hi = (v0hi + v1hi + (((v0lo >>> 0) + (v1lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0;
+        v0lo = (v0lo + v1lo) | 0;
+        // rotl(v1, 13)
+        let v1lo_ = v1lo;
+        let v1hi_ = v1hi;
+        v1lo = (v1lo_ << 13) | (v1hi_ >>> 19);
+        v1hi = (v1hi_ << 13) | (v1lo_ >>> 19);
+        // v1 ^= v0
+        v1lo ^= v0lo;
+        v1hi ^= v0hi;
+        // rotl(v0, 32)
+        const v0lo_ = v0lo;
+        const v0hi_ = v0hi;
+        v0lo = v0hi_;
+        v0hi = v0lo_;
+        // v2 += v3
+        v2hi = (v2hi + v3hi + (((v2lo >>> 0) + (v3lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0;
+        v2lo = (v2lo + v3lo) | 0;
+        // rotl(v3, 16)
+        let v3lo_ = v3lo;
+        let v3hi_ = v3hi;
+        v3lo = (v3lo_ << 16) | (v3hi_ >>> 16);
+        v3hi = (v3hi_ << 16) | (v3lo_ >>> 16);
+        // v3 ^= v2
+        v3lo ^= v2lo;
+        v3hi ^= v2hi;
+        // v0 += v3
+        v0hi = (v0hi + v3hi + (((v0lo >>> 0) + (v3lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0;
+        v0lo = (v0lo + v3lo) | 0;
+        // rotl(v3, 21)
+        v3lo_ = v3lo;
+        v3hi_ = v3hi;
+        v3lo = (v3lo_ << 21) | (v3hi_ >>> 11);
+        v3hi = (v3hi_ << 21) | (v3lo_ >>> 11);
+        // v3 ^= v0
+        v3lo ^= v0lo;
+        v3hi ^= v0hi;
+        // v2 += v1
+        v2hi = (v2hi + v1hi + (((v2lo >>> 0) + (v1lo >>> 0) > 0xffffffff) ? 1 : 0)) | 0;
+        v2lo = (v2lo + v1lo) | 0;
+        // rotl(v1, 17)
+        v1lo_ = v1lo;
+        v1hi_ = v1hi;
+        v1lo = (v1lo_ << 17) | (v1hi_ >>> 15);
+        v1hi = (v1hi_ << 17) | (v1lo_ >>> 15);
+        // v1 ^= v2
+        v1lo ^= v2lo;
+        v1hi ^= v2hi;
+        // rotl(v2, 32)
+        const v2lo_ = v2lo;
+        const v2hi_ = v2hi;
+        v2lo = v2hi_;
+        v2hi = v2lo_;
+    }
+}
+
+//////////////
+
+
+// Parts of this code are based on Lucene, which is licensed under the
+// Apache/2.0 license.
+// More information found here:
+// https://fossies.org/linux/lucene/lucene/core/src/java/org/apache/lucene/util/automaton/
+//   LevenshteinAutomata.java
+class ParametricDescription {
+    /**
+     * @param {number} w
+     * @param {number} n
+     * @param {Int32Array} minErrors
+     */
+    constructor(w, n, minErrors) {
+        this.w = w;
+        this.n = n;
+        this.minErrors = minErrors;
+    }
+    /**
+     * @param {number} absState
+     * @returns {boolean}
+     */
+    isAccept(absState) {
+        const state = Math.floor(absState / (this.w + 1));
+        const offset = absState % (this.w + 1);
+        return this.w - offset + this.minErrors[state] <= this.n;
+    }
+    /**
+     * @param {number} absState
+     * @returns {number}
+     */
+    getPosition(absState) {
+        return absState % (this.w + 1);
+    }
+    /**
+     * @param {Uint8Array} name
+     * @param {number} charCode
+     * @param {number} pos
+     * @param {number} end
+     * @returns {number}
+     */
+    getVector(name, charCode, pos, end) {
+        let vector = 0;
+        for (let i = pos; i < end; i += 1) {
+            vector = vector << 1;
+            if (name[i] === charCode) {
+                vector |= 1;
+            }
+        }
+        return vector;
+    }
+    /**
+     * @param {Int32Array} data
+     * @param {number} index
+     * @param {number} bitsPerValue
+     * @returns {number}
+     */
+    unpack(data, index, bitsPerValue) {
+        const bitLoc = (bitsPerValue * index);
+        const dataLoc = bitLoc >> 5;
+        const bitStart = bitLoc & 31;
+        if (bitStart + bitsPerValue <= 32) {
+            // not split
+            return ((data[dataLoc] >> bitStart) & this.MASKS[bitsPerValue - 1]);
+        } else {
+            // split
+            const part = 32 - bitStart;
+            return ~~(((data[dataLoc] >> bitStart) & this.MASKS[part - 1]) +
+                ((data[1 + dataLoc] & this.MASKS[bitsPerValue - part - 1]) << part));
+        }
+    }
+}
+ParametricDescription.prototype.MASKS = new Int32Array([
+    0x1, 0x3, 0x7, 0xF,
+    0x1F, 0x3F, 0x7F, 0xFF,
+    0x1FF, 0x3F, 0x7FF, 0xFFF,
+    0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF,
+    0x1FFFF, 0x3FFFF, 0x7FFFF, 0xFFFFF,
+    0x1FFFFF, 0x3FFFFF, 0x7FFFFF, 0xFFFFFF,
+    0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF,
+    0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF,
+]);
+
+// The following code was generated with the moman/finenight pkg
+// This package is available under the MIT License, see NOTICE.txt
+// for more details.
+// This class is auto-generated, Please do not modify it directly.
+// You should modify the https://gitlab.com/notriddle/createAutomata.py instead.
+// The following code was generated with the moman/finenight pkg
+// This package is available under the MIT License, see NOTICE.txt
+// for more details.
+// This class is auto-generated, Please do not modify it directly.
+// You should modify https://gitlab.com/notriddle/moman-rustdoc instead.
+
+class Lev2TParametricDescription extends ParametricDescription {
+    /**
+     * @param {number} absState
+     * @param {number} position
+     * @param {number} vector
+     * @returns {number}
+    */
+    transition(absState, position, vector) {
+        let state = Math.floor(absState / (this.w + 1));
+        let offset = absState % (this.w + 1);
+
+        if (position === this.w) {
+            if (state < 3) {
+                const loc = Math.imul(vector, 3) + state;
+                offset += this.unpack(this.offsetIncrs0, loc, 1);
+                state = this.unpack(this.toStates0, loc, 2) - 1;
+            }
+        } else if (position === this.w - 1) {
+            if (state < 5) {
+                const loc = Math.imul(vector, 5) + state;
+                offset += this.unpack(this.offsetIncrs1, loc, 1);
+                state = this.unpack(this.toStates1, loc, 3) - 1;
+            }
+        } else if (position === this.w - 2) {
+            if (state < 13) {
+                const loc = Math.imul(vector, 13) + state;
+                offset += this.unpack(this.offsetIncrs2, loc, 2);
+                state = this.unpack(this.toStates2, loc, 4) - 1;
+            }
+        } else if (position === this.w - 3) {
+            if (state < 28) {
+                const loc = Math.imul(vector, 28) + state;
+                offset += this.unpack(this.offsetIncrs3, loc, 2);
+                state = this.unpack(this.toStates3, loc, 5) - 1;
+            }
+        } else if (position === this.w - 4) {
+            if (state < 45) {
+                const loc = Math.imul(vector, 45) + state;
+                offset += this.unpack(this.offsetIncrs4, loc, 3);
+                state = this.unpack(this.toStates4, loc, 6) - 1;
+            }
+        } else {
+            // eslint-disable-next-line no-lonely-if
+            if (state < 45) {
+                const loc = Math.imul(vector, 45) + state;
+                offset += this.unpack(this.offsetIncrs5, loc, 3);
+                state = this.unpack(this.toStates5, loc, 6) - 1;
+            }
+        }
+
+        if (state === -1) {
+            // null state
+            return -1;
+        } else {
+            // translate back to abs
+            return Math.imul(state, this.w + 1) + offset;
+        }
+    }
+
+    // state map
+    //   0 -> [(0, 0)]
+    //   1 -> [(0, 1)]
+    //   2 -> [(0, 2)]
+    //   3 -> [(0, 1), (1, 1)]
+    //   4 -> [(0, 2), (1, 2)]
+    //   5 -> [(0, 1), (1, 1), (2, 1)]
+    //   6 -> [(0, 2), (1, 2), (2, 2)]
+    //   7 -> [(0, 1), (2, 1)]
+    //   8 -> [(0, 1), (2, 2)]
+    //   9 -> [(0, 2), (2, 1)]
+    //   10 -> [(0, 2), (2, 2)]
+    //   11 -> [t(0, 1), (0, 1), (1, 1), (2, 1)]
+    //   12 -> [t(0, 2), (0, 2), (1, 2), (2, 2)]
+    //   13 -> [(0, 2), (1, 2), (2, 2), (3, 2)]
+    //   14 -> [(0, 1), (1, 1), (3, 2)]
+    //   15 -> [(0, 1), (2, 2), (3, 2)]
+    //   16 -> [(0, 1), (3, 2)]
+    //   17 -> [(0, 1), t(1, 2), (2, 2), (3, 2)]
+    //   18 -> [(0, 2), (1, 2), (3, 1)]
+    //   19 -> [(0, 2), (1, 2), (3, 2)]
+    //   20 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2)]
+    //   21 -> [(0, 2), (2, 1), (3, 1)]
+    //   22 -> [(0, 2), (2, 2), (3, 2)]
+    //   23 -> [(0, 2), (3, 1)]
+    //   24 -> [(0, 2), (3, 2)]
+    //   25 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2)]
+    //   26 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2)]
+    //   27 -> [t(0, 2), (0, 2), (1, 2), (3, 1)]
+    //   28 -> [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
+    //   29 -> [(0, 2), (1, 2), (2, 2), (4, 2)]
+    //   30 -> [(0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)]
+    //   31 -> [(0, 2), (1, 2), (3, 2), (4, 2)]
+    //   32 -> [(0, 2), (1, 2), (4, 2)]
+    //   33 -> [(0, 2), (1, 2), t(1, 2), (2, 2), (3, 2), (4, 2)]
+    //   34 -> [(0, 2), (1, 2), t(2, 2), (2, 2), (3, 2), (4, 2)]
+    //   35 -> [(0, 2), (2, 1), (4, 2)]
+    //   36 -> [(0, 2), (2, 2), (3, 2), (4, 2)]
+    //   37 -> [(0, 2), (2, 2), (4, 2)]
+    //   38 -> [(0, 2), (3, 2), (4, 2)]
+    //   39 -> [(0, 2), (4, 2)]
+    //   40 -> [(0, 2), t(1, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
+    //   41 -> [(0, 2), t(2, 2), (2, 2), (3, 2), (4, 2)]
+    //   42 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (3, 2), (4, 2)]
+    //   43 -> [t(0, 2), (0, 2), (1, 2), (2, 2), (4, 2)]
+    //   44 -> [t(0, 2), (0, 2), (1, 2), (2, 2), t(2, 2), (3, 2), (4, 2)]
+
+
+    /** @param {number} w - length of word being checked */
+    constructor(w) {
+        super(w, 2, new Int32Array([
+            0,1,2,0,1,-1,0,-1,0,-1,0,-1,0,-1,-1,-1,-1,-1,-2,-1,-1,-2,-1,-2,
+            -1,-1,-1,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,
+        ]));
+    }
+}
+
+Lev2TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([
+    0xe,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([
+    0x0,
+]);
+
+Lev2TParametricDescription.prototype.toStates1 = /*3 bits per value */ new Int32Array([
+    0x1a688a2c,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([
+    0x3e0,
+]);
+
+Lev2TParametricDescription.prototype.toStates2 = /*4 bits per value */ new Int32Array([
+    0x70707054,0xdc07035,0x3dd3a3a,0x2323213a,
+    0x15435223,0x22545432,0x5435,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([
+    0x80000,0x55582088,0x55555555,0x55,
+]);
+
+Lev2TParametricDescription.prototype.toStates3 = /*5 bits per value */ new Int32Array([
+    0x1c0380a4,0x700a570,0xca529c0,0x180a00,
+    0xa80af180,0xc5498e60,0x5a546398,0x8c4300e8,
+    0xac18c601,0xd8d43501,0x863500ad,0x51976d6a,
+    0x8ca0180a,0xc3501ac2,0xb0c5be16,0x76dda8a5,
+    0x18c4519,0xc41294a,0xe248d231,0x1086520c,
+    0xce31ac42,0x13946358,0x2d0348c4,0x6732d494,
+    0x1ad224a5,0xd635ad4b,0x520c4139,0xce24948,
+    0x22110a52,0x58ce729d,0xc41394e3,0x941cc520,
+    0x90e732d4,0x4729d224,0x39ce35ad,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([
+    0x80000,0xc0c830,0x300f3c30,0x2200fcff,
+    0xcaa00a08,0x3c2200a8,0xa8fea00a,0x55555555,
+    0x55555555,0x55555555,0x55555555,0x55555555,
+    0x55555555,0x55555555,
+]);
+
+Lev2TParametricDescription.prototype.toStates4 = /*6 bits per value */ new Int32Array([
+    0x801c0144,0x1453803,0x14700038,0xc0005145,
+    0x1401,0x14,0x140000,0x0,
+    0x510000,0x6301f007,0x301f00d1,0xa186178,
+    0xc20ca0c3,0xc20c30,0xc30030c,0xc00c00cd,
+    0xf0c00c30,0x4c054014,0xc30944c3,0x55150c34,
+    0x8300550,0x430c0143,0x50c31,0xc30850c,
+    0xc3143000,0x50053c50,0x5130d301,0x850d30c2,
+    0x30a08608,0xc214414,0x43142145,0x21450031,
+    0x1400c314,0x4c143145,0x32832803,0x28014d6c,
+    0xcd34a0c3,0x1c50c76,0x1c314014,0x430c30c3,
+    0x1431,0xc300500,0xca00d303,0xd36d0e40,
+    0x90b0e400,0xcb2abb2c,0x70c20ca1,0x2c32ca2c,
+    0xcd2c70cb,0x31c00c00,0x34c2c32c,0x5583280,
+    0x558309b7,0x6cd6ca14,0x430850c7,0x51c51401,
+    0x1430c714,0xc3087,0x71451450,0xca00d30,
+    0xc26dc156,0xb9071560,0x1cb2abb2,0xc70c2144,
+    0xb1c51ca1,0x1421c70c,0xc51c00c3,0x30811c51,
+    0x24324308,0xc51031c2,0x70820820,0x5c33830d,
+    0xc33850c3,0x30c30c30,0xc30c31c,0x451450c3,
+    0x20c20c20,0xda0920d,0x5145914f,0x36596114,
+    0x51965865,0xd9643653,0x365a6590,0x51964364,
+    0x43081505,0x920b2032,0x2c718b28,0xd7242249,
+    0x35cb28b0,0x2cb3872c,0x972c30d7,0xb0c32cb2,
+    0x4e1c75c,0xc80c90c2,0x62ca2482,0x4504171c,
+    0xd65d9610,0x33976585,0xd95cb5d,0x4b5ca5d7,
+    0x73975c36,0x10308138,0xc2245105,0x41451031,
+    0x14e24208,0xc35c3387,0x51453851,0x1c51c514,
+    0xc70c30c3,0x20451450,0x14f1440c,0x4f0da092,
+    0x4513d41,0x6533944d,0x1350e658,0xe1545055,
+    0x64365a50,0x5519383,0x51030815,0x28920718,
+    0x441c718b,0x714e2422,0x1c35cb28,0x4e1c7387,
+    0xb28e1c51,0x5c70c32c,0xc204e1c7,0x81c61440,
+    0x1c62ca24,0xd04503ce,0x85d63944,0x39338e65,
+    0x8e154387,0x364b5ca3,0x38739738,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs4 = /*3 bits per value */ new Int32Array([
+    0x10000000,0xc00000,0x60061,0x400,
+    0x0,0x80010008,0x249248a4,0x8229048,
+    0x2092,0x6c3603,0xb61b6c30,0x6db6036d,
+    0xdb6c0,0x361b0180,0x91b72000,0xdb11b71b,
+    0x6db6236,0x1008200,0x12480012,0x24924906,
+    0x48200049,0x80410002,0x24000900,0x4924a489,
+    0x10822492,0x20800125,0x48360,0x9241b692,
+    0x6da4924,0x40009268,0x241b010,0x291b4900,
+    0x6d249249,0x49493423,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x2492,
+]);
+
+Lev2TParametricDescription.prototype.toStates5 = /*6 bits per value */ new Int32Array([
+    0x801c0144,0x1453803,0x14700038,0xc0005145,
+    0x1401,0x14,0x140000,0x0,
+    0x510000,0x4e00e007,0xe0051,0x3451451c,
+    0xd015000,0x30cd0000,0xc30c30c,0xc30c30d4,
+    0x40c30c30,0x7c01c014,0xc03458c0,0x185e0c07,
+    0x2830c286,0x830c3083,0xc30030,0x33430c,
+    0x30c3003,0x70051030,0x16301f00,0x8301f00d,
+    0x30a18617,0xc20ca0c,0x431420c3,0xb1450c51,
+    0x14314315,0x4f143145,0x34c05401,0x4c30944c,
+    0x55150c3,0x30830055,0x1430c014,0xc00050c3,
+    0xc30850,0xc314300,0x150053c5,0x25130d30,
+    0x5430d30c,0xc0354154,0x300d0c90,0x1cb2cd0c,
+    0xc91cb0c3,0x72c30cb2,0x14f1cb2c,0xc34c0540,
+    0x34c30944,0x82182214,0x851050c2,0x50851430,
+    0x1400c50c,0x30c5085,0x50c51450,0x150053c,
+    0xc25130d3,0x8850d30,0x1430a086,0x450c2144,
+    0x51cb1c21,0x1c91c70c,0xc71c314b,0x34c1cb1,
+    0x6c328328,0xc328014d,0x76cd34a0,0x1401c50c,
+    0xc31c3140,0x31430c30,0x14,0x30c3005,
+    0xa0ca00d3,0x535b0c,0x4d2830ca,0x514369b3,
+    0xc500d01,0x5965965a,0x30d46546,0x6435030c,
+    0x8034c659,0xdb439032,0x2c390034,0xcaaecb24,
+    0x30832872,0xcb28b1c,0x4b1c32cb,0x70030033,
+    0x30b0cb0c,0xe40ca00d,0x400d36d0,0xb2c90b0e,
+    0xca1cb2ab,0xa2c70c20,0x6575d95c,0x4315b5ce,
+    0x95c53831,0x28034c5d,0x9b705583,0xa1455830,
+    0xc76cd6c,0x40143085,0x71451c51,0x871430c,
+    0x450000c3,0xd3071451,0x1560ca00,0x560c26dc,
+    0xb35b2851,0xc914369,0x1a14500d,0x46593945,
+    0xcb2c939,0x94507503,0x328034c3,0x9b70558,
+    0xe41c5583,0x72caaeca,0x1c308510,0xc7147287,
+    0x50871c32,0x1470030c,0xd307147,0xc1560ca0,
+    0x1560c26d,0xabb2b907,0x21441cb2,0x38a1c70c,
+    0x8e657394,0x314b1c93,0x39438738,0x43083081,
+    0x31c22432,0x820c510,0x830d7082,0x50c35c33,
+    0xc30c338,0xc31c30c3,0x50c30c30,0xc204514,
+    0x890c90c2,0x31440c70,0xa8208208,0xea0df0c3,
+    0x8a231430,0xa28a28a2,0x28a28a1e,0x1861868a,
+    0x48308308,0xc3682483,0x14516453,0x4d965845,
+    0xd4659619,0x36590d94,0xd969964,0x546590d9,
+    0x20c20541,0x920d20c,0x5914f0da,0x96114514,
+    0x65865365,0xe89d3519,0x99e7a279,0x9e89e89e,
+    0x81821827,0xb2032430,0x18b28920,0x422492c7,
+    0xb28b0d72,0x3872c35c,0xc30d72cb,0x32cb2972,
+    0x1c75cb0c,0xc90c204e,0xa2482c80,0x24b1c62c,
+    0xc3a89089,0xb0ea2e42,0x9669a31c,0xa4966a28,
+    0x59a8a269,0x8175e7a,0xb203243,0x718b2892,
+    0x4114105c,0x17597658,0x74ce5d96,0x5c36572d,
+    0xd92d7297,0xe1ce5d70,0xc90c204,0xca2482c8,
+    0x4171c62,0x5d961045,0x976585d6,0x79669533,
+    0x964965a2,0x659689e6,0x308175e7,0x24510510,
+    0x451031c2,0xe2420841,0x5c338714,0x453851c3,
+    0x51c51451,0xc30c31c,0x451450c7,0x41440c20,
+    0xc708914,0x82105144,0xf1c58c90,0x1470ea0d,
+    0x61861863,0x8a1e85e8,0x8687a8a2,0x3081861,
+    0x24853c51,0x5053c368,0x1341144f,0x96194ce5,
+    0x1544d439,0x94385514,0xe0d90d96,0x5415464,
+    0x4f1440c2,0xf0da0921,0x4513d414,0x533944d0,
+    0x350e6586,0x86082181,0xe89e981d,0x18277689,
+    0x10308182,0x89207185,0x41c718b2,0x14e24224,
+    0xc35cb287,0xe1c73871,0x28e1c514,0xc70c32cb,
+    0x204e1c75,0x1c61440c,0xc62ca248,0x90891071,
+    0x2e41c58c,0xa31c70ea,0xe86175e7,0xa269a475,
+    0x5e7a57a8,0x51030817,0x28920718,0xf38718b,
+    0xe5134114,0x39961758,0xe1ce4ce,0x728e3855,
+    0x5ce0d92d,0xc204e1ce,0x81c61440,0x1c62ca24,
+    0xd04503ce,0x85d63944,0x75338e65,0x5d86075e,
+    0x89e69647,0x75e76576,
+]);
+Lev2TParametricDescription.prototype.offsetIncrs5 = /*3 bits per value */ new Int32Array([
+    0x10000000,0xc00000,0x60061,0x400,
+    0x0,0x60000008,0x6b003080,0xdb6ab6db,
+    0x2db6,0x800400,0x49245240,0x11482412,
+    0x104904,0x40020000,0x92292000,0xa4b25924,
+    0x9649658,0xd80c000,0xdb0c001b,0x80db6d86,
+    0x6db01b6d,0xc0600003,0x86000d86,0x6db6c36d,
+    0xddadb6ed,0x300001b6,0x6c360,0xe37236e4,
+    0x46db6236,0xdb6c,0x361b018,0xb91b7200,
+    0x6dbb1b71,0x6db763,0x20100820,0x61248001,
+    0x92492490,0x24820004,0x8041000,0x92400090,
+    0x24924830,0x555b6a49,0x2080012,0x20004804,
+    0x49252449,0x84112492,0x4000928,0x240201,
+    0x92922490,0x58924924,0x49456,0x120d8082,
+    0x6da4800,0x69249249,0x249a01b,0x6c04100,
+    0x6d240009,0x92492483,0x24d5adb4,0x60208001,
+    0x92000483,0x24925236,0x6846da49,0x10400092,
+    0x241b0,0x49291b49,0x636d2492,0x92494935,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,0x49249249,
+    0x92492492,0x24924924,0x49249249,0x92492492,
+    0x24924924,0x49249249,0x92492492,0x24924924,
+    0x49249249,0x92492492,0x24924924,
+]);
+
+class Lev1TParametricDescription extends ParametricDescription {
+    /**
+     * @param {number} absState
+     * @param {number} position
+     * @param {number} vector
+     * @returns {number}
+    */
+    transition(absState, position, vector) {
+        let state = Math.floor(absState / (this.w + 1));
+        let offset = absState % (this.w + 1);
+
+        if (position === this.w) {
+            if (state < 2) {
+                const loc = Math.imul(vector, 2) + state;
+                offset += this.unpack(this.offsetIncrs0, loc, 1);
+                state = this.unpack(this.toStates0, loc, 2) - 1;
+            }
+        } else if (position === this.w - 1) {
+            if (state < 3) {
+                const loc = Math.imul(vector, 3) + state;
+                offset += this.unpack(this.offsetIncrs1, loc, 1);
+                state = this.unpack(this.toStates1, loc, 2) - 1;
+            }
+        } else if (position === this.w - 2) {
+            if (state < 6) {
+                const loc = Math.imul(vector, 6) + state;
+                offset += this.unpack(this.offsetIncrs2, loc, 2);
+                state = this.unpack(this.toStates2, loc, 3) - 1;
+            }
+        } else {
+            // eslint-disable-next-line no-lonely-if
+            if (state < 6) {
+                const loc = Math.imul(vector, 6) + state;
+                offset += this.unpack(this.offsetIncrs3, loc, 2);
+                state = this.unpack(this.toStates3, loc, 3) - 1;
+            }
+        }
+
+        if (state === -1) {
+            // null state
+            return -1;
+        } else {
+            // translate back to abs
+            return Math.imul(state, this.w + 1) + offset;
+        }
+    }
+
+    // state map
+    //   0 -> [(0, 0)]
+    //   1 -> [(0, 1)]
+    //   2 -> [(0, 1), (1, 1)]
+    //   3 -> [(0, 1), (1, 1), (2, 1)]
+    //   4 -> [(0, 1), (2, 1)]
+    //   5 -> [t(0, 1), (0, 1), (1, 1), (2, 1)]
+
+
+    /** @param {number} w - length of word being checked */
+    constructor(w) {
+        super(w, 1, new Int32Array([0,1,0,-1,-1,-1]));
+    }
+}
+
+Lev1TParametricDescription.prototype.toStates0 = /*2 bits per value */ new Int32Array([
+    0x2,
+]);
+Lev1TParametricDescription.prototype.offsetIncrs0 = /*1 bits per value */ new Int32Array([
+    0x0,
+]);
+
+Lev1TParametricDescription.prototype.toStates1 = /*2 bits per value */ new Int32Array([
+    0xa43,
+]);
+Lev1TParametricDescription.prototype.offsetIncrs1 = /*1 bits per value */ new Int32Array([
+    0x38,
+]);
+
+Lev1TParametricDescription.prototype.toStates2 = /*3 bits per value */ new Int32Array([
+    0x12180003,0xb45a4914,0x69,
+]);
+Lev1TParametricDescription.prototype.offsetIncrs2 = /*2 bits per value */ new Int32Array([
+    0x558a0000,0x5555,
+]);
+
+Lev1TParametricDescription.prototype.toStates3 = /*3 bits per value */ new Int32Array([
+    0x900c0003,0xa1904864,0x45a49169,0x5a6d196a,
+    0x9634,
+]);
+Lev1TParametricDescription.prototype.offsetIncrs3 = /*2 bits per value */ new Int32Array([
+    0xa0fc0000,0x5555ba08,0x55555555,
+]);
diff --git a/src/librustdoc/html/static/js/tsconfig.json b/src/librustdoc/html/static/js/tsconfig.json
index b81099bb9df..42993cb0f2a 100644
--- a/src/librustdoc/html/static/js/tsconfig.json
+++ b/src/librustdoc/html/static/js/tsconfig.json
@@ -10,6 +10,6 @@
     "skipLibCheck": true
   },
   "typeAcquisition": {
-    "include": ["./rustdoc.d.ts"]
+    "include": ["./rustdoc.d.ts", "./stringdex.d.ts"]
   }
 }