diff --git a/apps/web/src/app/(tilt)/custom/marketplace/page.tsx b/apps/web/src/app/(tilt)/custom/marketplace/page.tsx
index 7bb1d4f5..184b097f 100644
--- a/apps/web/src/app/(tilt)/custom/marketplace/page.tsx
+++ b/apps/web/src/app/(tilt)/custom/marketplace/page.tsx
@@ -8,8 +8,8 @@ import { CustomizationPage } from "@/components/custom/CustomizationPage";
 import { IndexStoreResults } from "@/components/tilt-pro/IndexStoreResults";
 import { IndexStoreToolbar } from "@/components/tilt-pro/IndexStoreToolbar";
 import { useOrganization } from "@/contexts/OrganizationContext";
-import { useAssetSearch } from "@/hooks/useAssetSearch";
 import { useIndexEnabledList } from "@/hooks/useIndexEnabledList";
+import { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import { cn } from "@/lib/utils";
 import type { IndexStoreSortOption } from "@/lib/utils/index-store-converters";
 
@@ -21,7 +21,7 @@ export default function MarketplacePage() {
   const [sortOption, setSortOption] =
     useState<IndexStoreSortOption>("relevance");
 
-  const searchState = useAssetSearch({
+  const searchState = useTokenAssetSearch({
     limit: 50,
     allowedSourceTypes: ["TILT_INDEX"],
   });
diff --git a/apps/web/src/components/asset-search/AssetSearchInput.tsx b/apps/web/src/components/asset-search/AssetSearchInput.tsx
deleted file mode 100644
index 7b54604c..00000000
--- a/apps/web/src/components/asset-search/AssetSearchInput.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-"use client";
-
-import { LexicalComposer } from "@lexical/react/LexicalComposer";
-import { SearchIcon } from "@tilt/ui/icons";
-import { KeywordNode } from "@/lib/lexical/KeywordNode";
-import type { ParsedFiltersWithMatches } from "@/lib/utils/parseAssetSearchQuery";
-import { EditorContent } from "./EditorContent";
-
-type AssetSearchInputProps = {
-  value: string;
-  onChange: (value: string) => void;
-  parsedFilters: ParsedFiltersWithMatches | null;
-  isSearching: boolean;
-  onSearch?: () => void;
-  placeholder?: string;
-  className?: string;
-  disabled?: boolean;
-};
-
-export function AssetSearchInput(
-  props: AssetSearchInputProps
-): React.ReactElement {
-  const { className, disabled, isSearching } = props;
-
-  const initialConfig = {
-    namespace: "AssetSearchInputLexical",
-    theme: {
-      paragraph: "m-0 text-[14px]",
-      text: {
-        base: "text-white",
-      },
-    },
-    onError: (error: Error) => {
-      console.error("Lexical error:", error);
-    },
-    nodes: [KeywordNode],
-    editable: !disabled,
-  };
-
-  const isLoading = isSearching;
-
-  return (
-    <div
-      className={`relative flex max-w-2xl flex-1 items-center justify-between gap-2 rounded-[100px] border border-[#2e2e2e] bg-[#111111] p-4 ${className || ""}`}
-    >
-      {/* Left side: Search icon + Lexical editor */}
-      <div className="flex min-w-0 flex-1 items-center gap-2">
-        <SearchIcon className="size-6 shrink-0 text-white opacity-50" />
-
-        <div className="relative min-w-0 flex-1">
-          <LexicalComposer initialConfig={initialConfig}>
-            <EditorContent
-              onChange={props.onChange}
-              onSearch={props.onSearch}
-              parsedFilters={props.parsedFilters}
-              placeholder={props.placeholder}
-              value={props.value}
-            />
-          </LexicalComposer>
-        </div>
-
-        {/* Loading indicator - positioned after text */}
-        {isLoading && (
-          <div className="size-4 shrink-0 animate-spin rounded-full border-2 border-white/50 border-t-transparent" />
-        )}
-      </div>
-    </div>
-  );
-}
diff --git a/apps/web/src/components/asset-search/EditorContent.tsx b/apps/web/src/components/asset-search/EditorContent.tsx
deleted file mode 100644
index a14adadb..00000000
--- a/apps/web/src/components/asset-search/EditorContent.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-"use client";
-
-import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
-import { ContentEditable } from "@lexical/react/LexicalContentEditable";
-import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
-import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
-import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
-import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
-import {
-  $getRoot,
-  type EditorState,
-  type ElementNode,
-  TextNode,
-} from "lexical";
-import { useEffect, useRef, useState } from "react";
-import type { KeywordVariant } from "@/lib/lexical/KeywordNode";
-import { KeywordPlugin } from "@/lib/lexical/KeywordPlugin";
-import type { ParsedFiltersWithMatches } from "@/lib/utils/parseAssetSearchQuery";
-import { SearchPlugin } from "./SearchPlugin";
-
-type EditorContentProps = {
-  value: string;
-  onChange: (value: string) => void;
-  parsedFilters: ParsedFiltersWithMatches | null;
-  onSearch?: () => void;
-  placeholder?: string;
-  disabled?: boolean;
-};
-
-export function EditorContent({
-  value,
-  onChange,
-  parsedFilters,
-  onSearch,
-  placeholder,
-}: EditorContentProps): React.ReactElement {
-  const [editor] = useLexicalComposerContext();
-  const [popup, setPopup] = useState<{
-    text: string;
-    variant: KeywordVariant;
-    x: number;
-    y: number;
-  } | null>(null);
-  const editorRef = useRef<HTMLDivElement>(null);
-
-  // Initialize editor with value
-  useEffect(() => {
-    if (!value) return;
-
-    // Check if editor is available before updating
-    try {
-      editor.update(() => {
-        const root = $getRoot();
-        const paragraph = root.getFirstChild();
-        if (paragraph && paragraph.getType() === "paragraph") {
-          const paragraphElement = paragraph as ElementNode;
-          paragraphElement.clear();
-          const textNode = new TextNode(value);
-          paragraphElement.append(textNode);
-        }
-      });
-    } catch (error) {
-      // Silently handle editor not being ready - will be initialized by OnChangePlugin
-      console.debug("Editor not ready for initialization:", error);
-    }
-  }, []); // Only on mount
-
-  // Listen for chip clicks
-  useEffect(() => {
-    const handleChipClick = (e: Event) => {
-      const customEvent = e as CustomEvent<{
-        text: string;
-        variant: KeywordVariant;
-        element: HTMLElement;
-      }>;
-      const { text, variant, element } = customEvent.detail;
-
-      // Get element position
-      const rect = element.getBoundingClientRect();
-      setPopup({
-        text,
-        variant,
-        x: rect.left,
-        y: rect.bottom + 4,
-      });
-    };
-
-    const editorElement = editorRef.current;
-    if (editorElement) {
-      editorElement.addEventListener("keyword-chip-click", handleChipClick);
-      return () => {
-        editorElement.removeEventListener(
-          "keyword-chip-click",
-          handleChipClick
-        );
-      };
-    }
-  }, []);
-
-  // Close popup on outside click
-  useEffect(() => {
-    if (!popup) return;
-
-    const handleClickOutside = () => {
-      setPopup(null);
-    };
-
-    document.addEventListener("click", handleClickOutside);
-    return () => {
-      document.removeEventListener("click", handleClickOutside);
-    };
-  }, [popup]);
-
-  function handleChange(editorState: EditorState): void {
-    editorState.read(() => {
-      const root = $getRoot();
-      const text = root.getTextContent();
-      onChange(text);
-    });
-  }
-
-  return (
-    <div className="relative" ref={editorRef}>
-      <PlainTextPlugin
-        contentEditable={
-          <ContentEditable
-            className="min-h-[24px] outline-none"
-            style={{
-              caretColor: "rgb(255 255 255)",
-              color: "rgb(255 255 255)",
-            }}
-          />
-        }
-        ErrorBoundary={LexicalErrorBoundary}
-        placeholder={
-          <div
-            className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-0 select-none"
-            style={{ color: "rgba(255, 255, 255, 0.5)" }}
-          >
-            {placeholder}
-          </div>
-        }
-      />
-      <OnChangePlugin onChange={handleChange} />
-      <HistoryPlugin />
-      <KeywordPlugin parsedFilters={parsedFilters} />
-      <SearchPlugin onSearch={onSearch} />
-
-      {/* Popup */}
-      {popup && (
-        <div
-          className="fixed z-50 rounded-lg border border-border bg-card p-4 shadow-lg"
-          onClick={(e) => e.stopPropagation()}
-          style={{
-            left: `${popup.x}px`,
-            top: `${popup.y}px`,
-          }}
-        >
-          <div className="space-y-2">
-            <div className="font-semibold text-sm">Filter Details</div>
-            <div className="space-y-1 text-sm">
-              <div>
-                <span className="text-muted-foreground">Type: </span>
-                <span className="capitalize">{popup.variant}</span>
-              </div>
-              <div>
-                <span className="text-muted-foreground">Value: </span>
-                <span>{popup.text}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      )}
-    </div>
-  );
-}
diff --git a/apps/web/src/components/asset-search/SearchPlugin.tsx b/apps/web/src/components/asset-search/SearchPlugin.tsx
deleted file mode 100644
index f2a05d02..00000000
--- a/apps/web/src/components/asset-search/SearchPlugin.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
-import { COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND } from "lexical";
-import { useEffect } from "react";
-
-type SearchPluginProps = {
-  onSearch?: () => void;
-};
-
-export function SearchPlugin({ onSearch }: SearchPluginProps): null {
-  const [editor] = useLexicalComposerContext();
-
-  useEffect(
-    () =>
-      editor.registerCommand(
-        KEY_ENTER_COMMAND,
-        (event) => {
-          event?.preventDefault();
-          onSearch?.();
-          return true;
-        },
-        COMMAND_PRIORITY_HIGH
-      ),
-    [editor, onSearch]
-  );
-
-  return null;
-}
diff --git a/apps/web/src/components/asset-search/asset-search.tsx b/apps/web/src/components/asset-search/asset-search.tsx
deleted file mode 100644
index 14416be2..00000000
--- a/apps/web/src/components/asset-search/asset-search.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-"use client";
-
-import { useAssetSearch } from "@/hooks/useAssetSearch";
-import type { SourceType } from "@/lib/utils/parseAssetSearchQuery";
-import { AssetSearchInput } from "./AssetSearchInput";
-
-type AssetSearchProps = {
-  placeholder?: string;
-  className?: string;
-  /**
-   * Restrict parsing to specific source types (e.g., ["TILT", "TILT_INDEX"])
-   * If not provided, all source types are allowed
-   */
-  allowedSourceTypes?: readonly SourceType[];
-} & (
-  | {
-      /**
-       * Provide search state from useAssetSearch hook for controlled usage.
-       * Use this when the parent needs access to search state (results, loading, etc).
-       */
-      searchState: ReturnType<typeof useAssetSearch>;
-      limit?: never;
-    }
-  | {
-      /**
-       * Let the component manage its own search state internally.
-       * Use this for standalone search inputs that don't need to expose state.
-       */
-      searchState?: never;
-      /**
-       * Maximum number of results to return
-       * @default 20
-       */
-      limit?: number;
-    }
-);
-
-/**
- * Asset search component with built-in query parsing and search execution.
- * Can be used in controlled mode (with searchState from useAssetSearch) or
- * uncontrolled mode (manages its own state internally).
- *
- * @example
- * ```tsx
- * // Controlled mode - parent manages state
- * function MyPage() {
- *   const searchState = useAssetSearch();
- *   return (
- *     <>
- *       <AssetSearch searchState={searchState} />
- *       <div>Results: {searchState.results.length}</div>
- *     </>
- *   );
- * }
- *
- * // Uncontrolled mode - component manages state internally
- * function MyPage() {
- *   return <AssetSearch placeholder="Search funds..." limit={50} />;
- * }
- * ```
- */
-export function AssetSearch(props: AssetSearchProps): React.ReactElement {
-  const {
-    placeholder = "What are you looking for?",
-    className,
-    allowedSourceTypes,
-  } = props;
-
-  // Always call the hook (rules of hooks), but only use it if not in controlled mode
-  const internalSearchState = useAssetSearch({
-    limit: "limit" in props ? props.limit : undefined,
-    allowedSourceTypes,
-  });
-
-  // Use external state if provided, otherwise use internal state
-  const searchState =
-    "searchState" in props && props.searchState
-      ? props.searchState
-      : internalSearchState;
-
-  const { query, setQuery, parsedFilters, isSearching, executeSearch } =
-    searchState;
-
-  async function handleSearch(): Promise<void> {
-    await executeSearch();
-  }
-
-  return (
-    <AssetSearchInput
-      className={className}
-      isSearching={isSearching}
-      onChange={setQuery}
-      onSearch={handleSearch}
-      parsedFilters={parsedFilters}
-      placeholder={placeholder}
-      value={query}
-    />
-  );
-}
diff --git a/apps/web/src/components/custom/CreateAssetClassModal.tsx b/apps/web/src/components/custom/CreateAssetClassModal.tsx
index 17fe736d..563787e2 100644
--- a/apps/web/src/components/custom/CreateAssetClassModal.tsx
+++ b/apps/web/src/components/custom/CreateAssetClassModal.tsx
@@ -7,7 +7,7 @@ import type { AssetClassRead, AssetClassWrite } from "@tilt/tilt-backend-api";
 import { useMemo, useState } from "react";
 import { useForm } from "react-hook-form";
 import { toastFactory } from "@/components/ui/toast-factory";
-import { useAssetSearch } from "@/hooks/useAssetSearch";
+import { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import {
   AlternativeEtfSearchStep,
   BenchmarkSearchStep,
@@ -44,12 +44,12 @@ export function CreateAssetClassModal({
     "select-benchmark" | "create-class" | "select-alternative"
   >("select-benchmark");
 
-  const searchState = useAssetSearch({
+  const searchState = useTokenAssetSearch({
     limit: 100,
     allowedSourceTypes: ["TILT_INDEX", "ETF"] as const,
   });
 
-  const alternativeEtfSearchState = useAssetSearch({
+  const alternativeEtfSearchState = useTokenAssetSearch({
     limit: 100,
     allowedSourceTypes: ["ETF"] as const,
   });
diff --git a/apps/web/src/components/custom/EditAssetClassModal.tsx b/apps/web/src/components/custom/EditAssetClassModal.tsx
index c3003aac..7a5a82e4 100644
--- a/apps/web/src/components/custom/EditAssetClassModal.tsx
+++ b/apps/web/src/components/custom/EditAssetClassModal.tsx
@@ -11,7 +11,7 @@ import { useEffect, useMemo, useState } from "react";
 import { useForm } from "react-hook-form";
 import { browserClient } from "@/api/browserClient";
 import { toastFactory } from "@/components/ui/toast-factory";
-import { useAssetSearch } from "@/hooks/useAssetSearch";
+import { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import {
   AlternativeEtfSearchStep,
   type AssetClassFormValues,
@@ -46,12 +46,12 @@ export function EditAssetClassModal({
     "edit-class" | "select-benchmark" | "select-alternative"
   >("edit-class");
 
-  const benchmarkSearchState = useAssetSearch({
+  const benchmarkSearchState = useTokenAssetSearch({
     limit: 100,
     allowedSourceTypes: ["TILT_INDEX", "ETF"] as const,
   });
 
-  const alternativeEtfSearchState = useAssetSearch({
+  const alternativeEtfSearchState = useTokenAssetSearch({
     limit: 100,
     allowedSourceTypes: ["ETF"] as const,
   });
diff --git a/apps/web/src/components/custom/SelectOrCreateAssetClassModal.tsx b/apps/web/src/components/custom/SelectOrCreateAssetClassModal.tsx
index 47f6c3e9..1bbdbeea 100644
--- a/apps/web/src/components/custom/SelectOrCreateAssetClassModal.tsx
+++ b/apps/web/src/components/custom/SelectOrCreateAssetClassModal.tsx
@@ -7,7 +7,7 @@ import type { AssetClassRead, AssetClassWrite } from "@tilt/tilt-backend-api";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { useForm } from "react-hook-form";
 import { toastFactory } from "@/components/ui/toast-factory";
-import { useAssetSearch } from "@/hooks/useAssetSearch";
+import { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import {
   type AssetClassFormValues,
   assetClassFormSchema,
@@ -34,7 +34,7 @@ export function SelectOrCreateAssetClassModal({
   );
   const [step, setStep] = useState<"form" | "select-benchmark">("form");
 
-  const searchState = useAssetSearch({
+  const searchState = useTokenAssetSearch({
     limit: 100,
     allowedSourceTypes: ["TILT_INDEX", "ETF"] as const,
   });
diff --git a/apps/web/src/components/custom/SelectTiltOrIndexModal.tsx b/apps/web/src/components/custom/SelectTiltOrIndexModal.tsx
index a7ac4cf7..7435ecdb 100644
--- a/apps/web/src/components/custom/SelectTiltOrIndexModal.tsx
+++ b/apps/web/src/components/custom/SelectTiltOrIndexModal.tsx
@@ -3,9 +3,9 @@
 import type { FundResult } from "@tilt/sig-api-client/schemas";
 import { Button } from "@tilt/ui/components/button";
 import { useEffect, useRef, useState } from "react";
-import { AssetSearch } from "@/components/asset-search/asset-search";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
-import { useAssetSearch } from "@/hooks/useAssetSearch";
+import { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import { SvgLogoT } from "../SvgLogoT";
 
 type SelectTiltOrIndexModalProps = {
@@ -29,14 +29,15 @@ export function SelectTiltOrIndexModal({
   onRemove,
   existingTiltUuids,
 }: SelectTiltOrIndexModalProps) {
-  const searchState = useAssetSearch({
+  const searchState = useTokenAssetSearch({
+    facetSources: ["TILT_INDEX"],
     limit: 100,
     allowedSourceTypes: ["TILT", "TILT_INDEX"] as const,
     additionalFilters: {
       tilt_portfolio_published: true,
     },
   });
-  const { results, isSearching, query, setQuery } = searchState;
+  const { clearResults, results, isSearching, query } = searchState;
 
   const [currentPage, setCurrentPage] = useState(1);
   const [initialSelectedUuids, setInitialSelectedUuids] = useState<string[]>(
@@ -44,8 +45,8 @@ export function SelectTiltOrIndexModal({
   );
   const prevIsOpenRef = useRef(isOpen);
 
-  // Capture initial selected UUIDs when modal opens (freeze on open, don't update on selection changes)
-  // Also clear search query when modal closes to prevent Lexical editor errors
+  // Capture initial selected UUIDs when modal opens (freeze on open, don't update on selection changes).
+  // Clear the token search when the modal closes so the next open starts fresh.
   useEffect(() => {
     // Detect transition from closed to open
     if (isOpen && !prevIsOpenRef.current) {
@@ -53,12 +54,12 @@ export function SelectTiltOrIndexModal({
     }
     // Detect transition from open to closed
     if (!isOpen && prevIsOpenRef.current) {
-      setQuery("");
+      clearResults();
     }
     prevIsOpenRef.current = isOpen;
     // existingTiltUuids is intentionally omitted - we only want to capture it on open transition, not track changes
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isOpen, setQuery]);
+  }, [isOpen, clearResults]);
 
   // Sort results: selected items first (based on initial state), then unselected
   const sortedResults = [...(results ?? [])].sort((a, b) => {
@@ -127,7 +128,7 @@ export function SelectTiltOrIndexModal({
       <div className="flex flex-col">
         {/* Search Bar */}
         <div className="px-6 pt-6">
-          <AssetSearch
+          <TokenAssetSearch
             className="max-w-none"
             placeholder="AI infrastructure, US mega-cap tech, emerging market dividend..."
             searchState={searchState}
diff --git a/apps/web/src/components/custom/create-asset-class/AlternativeEtfSearchStep.tsx b/apps/web/src/components/custom/create-asset-class/AlternativeEtfSearchStep.tsx
index af7880e9..fa3cbac3 100644
--- a/apps/web/src/components/custom/create-asset-class/AlternativeEtfSearchStep.tsx
+++ b/apps/web/src/components/custom/create-asset-class/AlternativeEtfSearchStep.tsx
@@ -2,8 +2,8 @@
 
 import { Form } from "@tilt/ui/components/form";
 import { ArrowLeftIcon, LoadingIcon } from "@tilt/ui/icons";
-import { AssetSearch } from "@/components/asset-search/asset-search";
 import { CustomizationDataTable } from "@/components/custom/CustomizationDataTable";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
 import type { CreateAlternativeEtfSearchStepProps } from "./types";
 
@@ -38,7 +38,7 @@ export function AlternativeEtfSearchStep({
       <Form {...form}>
         <form className="flex flex-col gap-6">
           <div className="flex flex-col gap-4">
-            <AssetSearch
+            <TokenAssetSearch
               className="max-w-none"
               placeholder="Search for ETFs..."
               searchState={searchState}
diff --git a/apps/web/src/components/custom/create-asset-class/BenchmarkSearchStep.tsx b/apps/web/src/components/custom/create-asset-class/BenchmarkSearchStep.tsx
index a0476710..c6a3f80d 100644
--- a/apps/web/src/components/custom/create-asset-class/BenchmarkSearchStep.tsx
+++ b/apps/web/src/components/custom/create-asset-class/BenchmarkSearchStep.tsx
@@ -2,8 +2,8 @@
 
 import { Form } from "@tilt/ui/components/form";
 import { ArrowLeftIcon, LoadingIcon } from "@tilt/ui/icons";
-import { AssetSearch } from "@/components/asset-search/asset-search";
 import { CustomizationDataTable } from "@/components/custom/CustomizationDataTable";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
 import type { CreateBenchmarkSearchStepProps } from "./types";
 
@@ -38,7 +38,7 @@ export function BenchmarkSearchStep({
       <Form {...form}>
         <form className="flex flex-col gap-6">
           <div className="flex flex-col gap-4">
-            <AssetSearch
+            <TokenAssetSearch
               className="max-w-none"
               placeholder="Search for indices and ETFs..."
               searchState={searchState}
diff --git a/apps/web/src/components/custom/create-asset-class/types.ts b/apps/web/src/components/custom/create-asset-class/types.ts
index f285c2c9..6ca92ea5 100644
--- a/apps/web/src/components/custom/create-asset-class/types.ts
+++ b/apps/web/src/components/custom/create-asset-class/types.ts
@@ -1,10 +1,10 @@
 import type { ColumnDef } from "@tanstack/react-table";
 import type { FundResult } from "@tilt/sig-api-client/schemas";
 import type { UseFormReturn } from "react-hook-form";
-import type { useAssetSearch } from "@/hooks/useAssetSearch";
+import type { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import type { AssetClassFormValues } from "../edit-asset-class/types";
 
-type AssetSearchState = ReturnType<typeof useAssetSearch>;
+type AssetSearchState = ReturnType<typeof useTokenAssetSearch>;
 
 export type CreateAssetClassFormStepProps = {
   isOpen: boolean;
diff --git a/apps/web/src/components/custom/edit-asset-class/AlternativeEtfSearchStep.tsx b/apps/web/src/components/custom/edit-asset-class/AlternativeEtfSearchStep.tsx
index c01bea68..51092c7b 100644
--- a/apps/web/src/components/custom/edit-asset-class/AlternativeEtfSearchStep.tsx
+++ b/apps/web/src/components/custom/edit-asset-class/AlternativeEtfSearchStep.tsx
@@ -2,8 +2,8 @@
 
 import { Form } from "@tilt/ui/components/form";
 import { ArrowLeftIcon, LoadingIcon } from "@tilt/ui/icons";
-import { AssetSearch } from "@/components/asset-search/asset-search";
 import { CustomizationDataTable } from "@/components/custom/CustomizationDataTable";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
 import type { AlternativeEtfSearchStepProps } from "./types";
 
@@ -38,7 +38,7 @@ export function AlternativeEtfSearchStep({
       <Form {...form}>
         <form className="flex flex-col gap-6">
           <div className="flex flex-col gap-4">
-            <AssetSearch
+            <TokenAssetSearch
               className="max-w-none"
               placeholder="Search for ETFs..."
               searchState={searchState}
diff --git a/apps/web/src/components/custom/edit-asset-class/BenchmarkSearchStep.tsx b/apps/web/src/components/custom/edit-asset-class/BenchmarkSearchStep.tsx
index 570f831d..f5b8aa04 100644
--- a/apps/web/src/components/custom/edit-asset-class/BenchmarkSearchStep.tsx
+++ b/apps/web/src/components/custom/edit-asset-class/BenchmarkSearchStep.tsx
@@ -2,8 +2,8 @@
 
 import { Form } from "@tilt/ui/components/form";
 import { ArrowLeftIcon, LoadingIcon } from "@tilt/ui/icons";
-import { AssetSearch } from "@/components/asset-search/asset-search";
 import { CustomizationDataTable } from "@/components/custom/CustomizationDataTable";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
 import type { BenchmarkSearchStepProps } from "./types";
 
@@ -38,7 +38,7 @@ export function BenchmarkSearchStep({
       <Form {...form}>
         <form className="flex flex-col gap-6">
           <div className="flex flex-col gap-4">
-            <AssetSearch
+            <TokenAssetSearch
               className="max-w-none"
               placeholder="Search for indices and ETFs..."
               searchState={searchState}
diff --git a/apps/web/src/components/custom/edit-asset-class/types.ts b/apps/web/src/components/custom/edit-asset-class/types.ts
index a6ecf257..76e0249f 100644
--- a/apps/web/src/components/custom/edit-asset-class/types.ts
+++ b/apps/web/src/components/custom/edit-asset-class/types.ts
@@ -3,7 +3,7 @@ import type { FundResult } from "@tilt/sig-api-client/schemas";
 import type { AssetClassRead } from "@tilt/tilt-backend-api";
 import type { UseFormReturn } from "react-hook-form";
 import * as z from "zod";
-import type { useAssetSearch } from "@/hooks/useAssetSearch";
+import type { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 
 export const assetClassFormSchema = z
   .object({
@@ -20,7 +20,7 @@ export const assetClassFormSchema = z
 
 export type AssetClassFormValues = z.infer<typeof assetClassFormSchema>;
 
-type AssetSearchState = ReturnType<typeof useAssetSearch>;
+type AssetSearchState = ReturnType<typeof useTokenAssetSearch>;
 
 export type BenchmarkSearchStepProps = {
   isOpen: boolean;
diff --git a/apps/web/src/components/custom/select-or-create-asset-class/BenchmarkSearchStep.tsx b/apps/web/src/components/custom/select-or-create-asset-class/BenchmarkSearchStep.tsx
index 6a774828..83adb31d 100644
--- a/apps/web/src/components/custom/select-or-create-asset-class/BenchmarkSearchStep.tsx
+++ b/apps/web/src/components/custom/select-or-create-asset-class/BenchmarkSearchStep.tsx
@@ -1,8 +1,8 @@
 "use client";
 
 import { ArrowLeftIcon, LoadingIcon } from "@tilt/ui/icons";
-import { AssetSearch } from "@/components/asset-search/asset-search";
 import { CustomizationDataTable } from "@/components/custom/CustomizationDataTable";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { BaseModal } from "@/components/ui/base-modal";
 import type { BenchmarkSearchStepProps } from "./types";
 
@@ -34,7 +34,7 @@ export function BenchmarkSearchStep({
       }
     >
       <div className="flex flex-col gap-6">
-        <AssetSearch
+        <TokenAssetSearch
           className="max-w-none"
           placeholder="Search for indices and ETFs..."
           searchState={searchState}
diff --git a/apps/web/src/components/custom/select-or-create-asset-class/types.ts b/apps/web/src/components/custom/select-or-create-asset-class/types.ts
index 6991e8d2..c9aa827a 100644
--- a/apps/web/src/components/custom/select-or-create-asset-class/types.ts
+++ b/apps/web/src/components/custom/select-or-create-asset-class/types.ts
@@ -3,7 +3,7 @@ import type { FundResult } from "@tilt/sig-api-client/schemas";
 import type { AssetClassRead } from "@tilt/tilt-backend-api";
 import type { UseFormReturn } from "react-hook-form";
 import * as z from "zod";
-import type { useAssetSearch } from "@/hooks/useAssetSearch";
+import type { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 
 export const assetClassFormSchema = z
   .object({
@@ -19,7 +19,7 @@ export const assetClassFormSchema = z
 
 export type AssetClassFormValues = z.infer<typeof assetClassFormSchema>;
 
-type AssetSearchState = ReturnType<typeof useAssetSearch>;
+type AssetSearchState = ReturnType<typeof useTokenAssetSearch>;
 
 export type SelectOrCreateAssetClassModalProps = {
   isOpen: boolean;
diff --git a/apps/web/src/components/store-search/TokenSearchBar.tsx b/apps/web/src/components/store-search/TokenSearchBar.tsx
index d973faf8..9fa6a6b8 100644
--- a/apps/web/src/components/store-search/TokenSearchBar.tsx
+++ b/apps/web/src/components/store-search/TokenSearchBar.tsx
@@ -90,6 +90,7 @@ export function TokenSearchBar({
     setFocusedTokenIndex,
     openDropdown,
     closeDropdown,
+    catalogue,
   } = tokenState;
 
   useEffect(() => {
@@ -162,7 +163,11 @@ export function TokenSearchBar({
     (tokenId: string, newDisplayText: string | null) => {
       stopEditingToken();
       if (newDisplayText) {
-        const newSuggestions = getTokenSuggestions(newDisplayText, tokens);
+        const newSuggestions = getTokenSuggestions(
+          newDisplayText,
+          tokens,
+          catalogue
+        );
         if (
           newSuggestions.length === 1 &&
           newSuggestions[0].confidence >= 0.95
@@ -183,7 +188,7 @@ export function TokenSearchBar({
       }
       inputRefs.current.get(activeSegmentId)?.focus();
     },
-    [stopEditingToken, updateToken, tokens, activeSegmentId]
+    [stopEditingToken, updateToken, tokens, catalogue, activeSegmentId]
   );
 
   /** Focus a text segment's input, placing the cursor at a given position. */
@@ -225,6 +230,7 @@ export function TokenSearchBar({
     recategorizeTokenId,
     constructionState,
     isConstructing,
+    catalogue,
     focusPrevToken,
     focusNextToken,
     removeToken,
diff --git a/apps/web/src/components/store-search/use-search-keyboard.ts b/apps/web/src/components/store-search/use-search-keyboard.ts
index 497a291d..efd65862 100644
--- a/apps/web/src/components/store-search/use-search-keyboard.ts
+++ b/apps/web/src/components/store-search/use-search-keyboard.ts
@@ -5,6 +5,7 @@ import {
   getScopedValueSuggestions,
   shouldSmartSpaceFinalize,
 } from "./facet-construction";
+import type { FilterDefinition } from "./filter-catalogue";
 import {
   findTextFromFocusedToken,
   getTokenSegmentInfo,
@@ -30,6 +31,7 @@ type SearchKeyboardDeps = {
   recategorizeTokenId: string | null;
   constructionState: TokenConstructionState;
   isConstructing: boolean;
+  catalogue?: FilterDefinition[];
 
   focusPrevToken: () => void;
   focusNextToken: () => void;
@@ -71,6 +73,7 @@ export function useSearchKeyboard(deps: SearchKeyboardDeps) {
     recategorizeTokenId,
     constructionState,
     isConstructing,
+    catalogue,
     focusPrevToken,
     focusNextToken,
     removeToken,
@@ -152,7 +155,8 @@ export function useSearchKeyboard(deps: SearchKeyboardDeps) {
               const scoped = getScopedValueSuggestions(
                 trimmed,
                 constructionState.category,
-                tokens
+                tokens,
+                catalogue
               );
               if (shouldSmartSpaceFinalize(scoped, trimmed)) {
                 e.preventDefault();
@@ -298,6 +302,7 @@ export function useSearchKeyboard(deps: SearchKeyboardDeps) {
       recategorizeTokenId,
       constructionState,
       isConstructing,
+      catalogue,
       activeSegmentId,
       segments,
       focusPrevToken,
diff --git a/apps/web/src/components/store-search/use-store-facets-catalogue.ts b/apps/web/src/components/store-search/use-store-facets-catalogue.ts
index 34fc2476..487c11e9 100644
--- a/apps/web/src/components/store-search/use-store-facets-catalogue.ts
+++ b/apps/web/src/components/store-search/use-store-facets-catalogue.ts
@@ -13,7 +13,7 @@ import {
   labelForStrategyCode,
   normalizeStrategyCode,
 } from "@tilt/store-search-core";
-import { useEffect, useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
 import {
   cloneFilterValue,
   normalizeDefault,
@@ -45,6 +45,18 @@ type StoreFacetsSnapshot = {
 type UseStoreFacetsCatalogueOptions = {
   sources?: FundsSearchFilters["source"];
   minCount?: number;
+  /**
+   * `TILT` is a client-only source chip for inbox-published Tilts. Store pages
+   * need it, but narrower pickers such as ETF/index selectors should not offer
+   * a chip that their `useAssetSearch` source filter will reject.
+   */
+  includeTiltSource?: boolean;
+  /**
+   * Existing store surfaces still read the shared singleton catalogue. Scoped
+   * per-instance searches should opt out and consume the returned catalogue
+   * instead so they do not alter unrelated search bars.
+   */
+  registerGlobal?: boolean;
 };
 
 function snapshotIsValid(value: unknown): value is StoreFacetsSnapshot {
@@ -156,10 +168,10 @@ function buildCategoryDefinition(
   };
 }
 
-function buildDynamicCatalogue(
-  facets: FundSearchFacetsResponse["facets"]
-): FilterDefinition[] {
-  const rawSourceValues = uniqueNormalizedValues(facets.source, normalizeUpper);
+function buildSourceValues(
+  rawSourceValues: readonly string[],
+  { includeTiltSource }: { includeTiltSource: boolean }
+): string[] {
   // EXTERNAL_INDEX is SIG's internal name for third-party indices — they are the
   // same "Index" concept as TILT_INDEX. Always include it alongside TILT_INDEX so
   // both codes are sent as filters when the user selects "Index", even when the
@@ -168,13 +180,46 @@ function buildDynamicCatalogue(
     rawSourceValues.includes("TILT_INDEX") &&
     !rawSourceValues.includes("EXTERNAL_INDEX")
       ? [...rawSourceValues, "EXTERNAL_INDEX"]
-      : rawSourceValues;
+      : [...rawSourceValues];
+
   // TILT represents inbox-published Tilts — a client-side-only concept that
-  // SIG's facets API will never return. Always inject it so the chip is
-  // available regardless of what SIG reports.
-  const sourceValues = sourceWithExternal.includes("TILT")
-    ? sourceWithExternal
-    : [...sourceWithExternal, "TILT"];
+  // SIG's facets API will never return. Store search needs it, but narrower
+  // selectors should not offer a chip that their source filter will reject.
+  return includeTiltSource && !sourceWithExternal.includes("TILT")
+    ? [...sourceWithExternal, "TILT"]
+    : sourceWithExternal;
+}
+
+function buildFallbackCatalogue(
+  sourceValues: readonly string[],
+  { includeTiltSource }: { includeTiltSource: boolean }
+): FilterDefinition[] {
+  const normalizedSourceValues = uniqueNormalizedValues(
+    sourceValues,
+    normalizeUpper
+  );
+
+  return BASELINE_FILTER_CATALOGUE.map((definition) =>
+    definition.category === "source"
+      ? buildCategoryDefinition(
+          "source",
+          buildSourceValues(normalizedSourceValues, { includeTiltSource })
+        )
+      : {
+          ...definition,
+          values: definition.values.map(cloneFilterValue),
+        }
+  );
+}
+
+function buildDynamicCatalogue(
+  facets: FundSearchFacetsResponse["facets"],
+  { includeTiltSource }: { includeTiltSource: boolean }
+): FilterDefinition[] {
+  const rawSourceValues = uniqueNormalizedValues(facets.source, normalizeUpper);
+  const sourceValues = buildSourceValues(rawSourceValues, {
+    includeTiltSource,
+  });
   const issuerValues = uniqueNormalizedValues(facets.issuer, normalizeDefault);
   const currencyValues = uniqueNormalizedValues(
     facets.fund_currency,
@@ -227,29 +272,45 @@ function writeSnapshot(snapshot: StoreFacetsSnapshot): void {
 export function useStoreFacetsCatalogue(
   options: UseStoreFacetsCatalogueOptions = {}
 ) {
-  const { sources = ["ETF", "TILT_INDEX", "MF"], minCount = 1 } = options;
+  const {
+    includeTiltSource = true,
+    minCount = 1,
+    registerGlobal = true,
+    sources = ["ETF", "TILT_INDEX", "MF"],
+  } = options;
 
-  const normalizedSources = useMemo(
-    () => [...new Set(sources.map((source) => source.toUpperCase()))].sort(),
+  const sourcesKey = useMemo(
+    () =>
+      [...new Set(sources.map((source) => source.toUpperCase()))]
+        .sort()
+        .join(","),
     [sources]
   );
-
-  const sourcesKey = useMemo(
-    () => normalizedSources.join(","),
-    [normalizedSources]
+  const normalizedSources = useMemo(
+    () => (sourcesKey ? sourcesKey.split(",") : []),
+    [sourcesKey]
   );
+  const fallbackCatalogue = useMemo(
+    () => buildFallbackCatalogue(normalizedSources, { includeTiltSource }),
+    [normalizedSources, includeTiltSource]
+  );
+  const [catalogue, setCatalogue] =
+    useState<FilterDefinition[]>(fallbackCatalogue);
 
   // Hydrate from localStorage whenever sourcesKey changes (including mount).
   // If no matching snapshot exists, reset to baseline so the singleton never
   // serves stale facets from a previous source set.
   useEffect(() => {
     const snapshot = readSnapshot();
-    if (snapshot && snapshot.sourcesKey === sourcesKey) {
-      setFilterCatalogue(buildDynamicCatalogue(snapshot.facets));
-    } else {
-      setFilterCatalogue(BASELINE_FILTER_CATALOGUE);
+    const nextCatalogue =
+      snapshot && snapshot.sourcesKey === sourcesKey
+        ? buildDynamicCatalogue(snapshot.facets, { includeTiltSource })
+        : fallbackCatalogue;
+    setCatalogue(nextCatalogue);
+    if (registerGlobal) {
+      setFilterCatalogue(nextCatalogue);
     }
-  }, [sourcesKey]);
+  }, [sourcesKey, fallbackCatalogue, includeTiltSource, registerGlobal]);
 
   const query = useQuery({
     queryKey: ["store-search", "facets", sourcesKey, minCount],
@@ -279,14 +340,20 @@ export function useStoreFacetsCatalogue(
   useEffect(() => {
     if (!query.data) return;
 
-    setFilterCatalogue(buildDynamicCatalogue(query.data.facets));
+    const nextCatalogue = buildDynamicCatalogue(query.data.facets, {
+      includeTiltSource,
+    });
+    setCatalogue(nextCatalogue);
+    if (registerGlobal) {
+      setFilterCatalogue(nextCatalogue);
+    }
     writeSnapshot({
       version: STORE_FACETS_STORAGE_VERSION,
       sourcesKey,
       fetchedAt: new Date().toISOString(),
       facets: query.data.facets,
     });
-  }, [query.data, sourcesKey]);
+  }, [query.data, sourcesKey, includeTiltSource, registerGlobal]);
 
-  return query;
+  return { ...query, catalogue };
 }
diff --git a/apps/web/src/components/tilt-pro/IndexStoreToolbar.tsx b/apps/web/src/components/tilt-pro/IndexStoreToolbar.tsx
index 0670cf93..62740699 100644
--- a/apps/web/src/components/tilt-pro/IndexStoreToolbar.tsx
+++ b/apps/web/src/components/tilt-pro/IndexStoreToolbar.tsx
@@ -1,13 +1,13 @@
 "use client";
 
-import { AssetSearch } from "@/components/asset-search/asset-search";
+import { TokenAssetSearch } from "@/components/store-search/TokenAssetSearch";
 import { IndexStoreSortSelector } from "@/components/tilt-pro/IndexStoreSortSelector";
 import { IndexStoreViewToggle } from "@/components/tilt-pro/IndexStoreViewToggle";
-import type { useAssetSearch } from "@/hooks/useAssetSearch";
+import type { useTokenAssetSearch } from "@/hooks/useTokenAssetSearch";
 import type { IndexStoreSortOption } from "@/lib/utils/index-store-converters";
 
 type IndexStoreToolbarProps = {
-  searchState: ReturnType<typeof useAssetSearch>;
+  searchState: ReturnType<typeof useTokenAssetSearch>;
   selectedView: string;
   onViewChange: (view: string) => void;
   hideSearch?: boolean;
@@ -30,7 +30,7 @@ export function IndexStoreToolbar({
   return (
     <div className="flex items-center justify-between gap-3">
       {!hideSearch && (
-        <AssetSearch
+        <TokenAssetSearch
           placeholder="Search indices and ETFs..."
           searchState={searchState}
         />
diff --git a/apps/web/src/hooks/useSearchTokens.ts b/apps/web/src/hooks/useSearchTokens.ts
index ba7a6478..9e238407 100644
--- a/apps/web/src/hooks/useSearchTokens.ts
+++ b/apps/web/src/hooks/useSearchTokens.ts
@@ -1,6 +1,7 @@
 "use client";
 
 import { useCallback, useMemo, useState } from "react";
+import type { FilterDefinition } from "@/components/store-search/filter-catalogue";
 import {
   buildParsedFilters,
   buildSegmentsFromItems,
@@ -28,6 +29,8 @@ import { useSuggestions } from "./useSearchTokens/useSuggestions";
 import { useTokenConstruction } from "./useSearchTokens/useTokenConstruction";
 
 type UseSearchTokensReturn = {
+  catalogue?: FilterDefinition[];
+
   // Segment state
   segments: Segment[];
   activeSegmentId: string;
@@ -92,13 +95,18 @@ type UseSearchTokensReturn = {
   loadItems: (items: DecodedItem[]) => void;
 };
 
+type UseSearchTokensOptions = {
+  blockedCategories?: FilterCategory[];
+  catalogue?: FilterDefinition[];
+};
+
 function makeInitialSegments(): Segment[] {
   return [{ type: "text", id: generateSegmentId(), value: "" }];
 }
 
 export function useSearchTokens(
   initialItems?: DecodedItem[],
-  options?: { blockedCategories?: FilterCategory[] }
+  options?: UseSearchTokensOptions
 ): UseSearchTokensReturn {
   const [segments, setSegments] = useState<Segment[]>(() =>
     initialItems ? buildSegmentsFromItems(initialItems) : makeInitialSegments()
@@ -164,6 +172,7 @@ export function useSearchTokens(
     dismissedTexts,
     constructionState: construction.constructionState,
     blockedCategories: options?.blockedCategories,
+    catalogue: options?.catalogue,
     setSegments,
     setActiveSegmentId,
     setDropdownOpen,
@@ -177,6 +186,7 @@ export function useSearchTokens(
     recategorizeTokenId,
     constructionState: construction.constructionState,
     blockedCategories: options?.blockedCategories,
+    catalogue: options?.catalogue,
   });
 
   // ─── Segment mutations ──────────────────────────────────────────────────────
@@ -351,6 +361,7 @@ export function useSearchTokens(
       tokens,
       activeTextSegment,
       addToken,
+      catalogue: options?.catalogue,
       setSegments,
       setDropdownOpen,
     });
@@ -440,6 +451,7 @@ export function useSearchTokens(
   );
 
   return {
+    catalogue: options?.catalogue,
     segments,
     activeSegmentId,
     setActiveSegmentId,
diff --git a/apps/web/src/hooks/useSearchTokens/useAutoTokenizeEffect.ts b/apps/web/src/hooks/useSearchTokens/useAutoTokenizeEffect.ts
index 1866001e..aa77f5e9 100644
--- a/apps/web/src/hooks/useSearchTokens/useAutoTokenizeEffect.ts
+++ b/apps/web/src/hooks/useSearchTokens/useAutoTokenizeEffect.ts
@@ -2,6 +2,7 @@
 
 import { ensureDefaultPeriod } from "@tilt/store-search-core/facets-utils";
 import { useEffect, useRef } from "react";
+import type { FilterDefinition } from "@/components/store-search/filter-catalogue";
 import {
   getTokenSuggestions,
   shouldAutoTokenize,
@@ -25,6 +26,7 @@ type UseAutoTokenizeEffectParams = {
   dismissedTexts: Set<string>;
   constructionState: TokenConstructionState;
   blockedCategories?: FilterCategory[];
+  catalogue?: FilterDefinition[];
   setSegments: React.Dispatch<React.SetStateAction<Segment[]>>;
   setActiveSegmentId: (id: string) => void;
   setDropdownOpen: (open: boolean) => void;
@@ -45,6 +47,7 @@ export function useAutoTokenizeEffect({
   dismissedTexts,
   constructionState,
   blockedCategories,
+  catalogue,
   setSegments,
   setActiveSegmentId,
   setDropdownOpen,
@@ -66,7 +69,7 @@ export function useAutoTokenizeEffect({
       const fullSuggestions = getTokenSuggestions(
         trimmed,
         tokens,
-        undefined,
+        catalogue,
         blockedCategories
       );
       const fullToken = shouldAutoTokenize(fullSuggestions, trimmed);
@@ -110,7 +113,7 @@ export function useAutoTokenizeEffect({
           const suggestions = getTokenSuggestions(
             phrase,
             tokens,
-            undefined,
+            catalogue,
             blockedCategories
           );
           const autoToken = shouldAutoTokenize(suggestions, phrase);
@@ -153,6 +156,7 @@ export function useAutoTokenizeEffect({
     dismissedTexts,
     constructionState,
     blockedCategories,
+    catalogue,
     setSegments,
     setActiveSegmentId,
     setDropdownOpen,
diff --git a/apps/web/src/hooks/useSearchTokens/useConstructionCallbacks.ts b/apps/web/src/hooks/useSearchTokens/useConstructionCallbacks.ts
index 4d822fd6..7a17b426 100644
--- a/apps/web/src/hooks/useSearchTokens/useConstructionCallbacks.ts
+++ b/apps/web/src/hooks/useSearchTokens/useConstructionCallbacks.ts
@@ -12,6 +12,7 @@ import {
   getOperatorsForCategory,
   getScopedValueSuggestions,
 } from "@/components/store-search/facet-construction";
+import type { FilterDefinition } from "@/components/store-search/filter-catalogue";
 import { canSerializeValueToken } from "@/components/store-search/token-serialization";
 import {
   CATEGORY_TO_VARIANT,
@@ -33,6 +34,7 @@ type ConstructionCallbacksDeps = {
   tokens: SearchToken[];
   activeTextSegment: TextSegment | null;
   addToken: (suggestion: TokenSuggestion) => void;
+  catalogue?: FilterDefinition[];
   setSegments: React.Dispatch<React.SetStateAction<Segment[]>>;
   setDropdownOpen: (open: boolean) => void;
 };
@@ -65,6 +67,7 @@ export function useConstructionCallbacks({
   tokens,
   activeTextSegment,
   addToken,
+  catalogue,
   setSegments,
   setDropdownOpen,
 }: ConstructionCallbacksDeps) {
@@ -225,7 +228,8 @@ export function useConstructionCallbacks({
       const scoped = getScopedValueSuggestions(
         trimmed,
         category,
-        tokensRef.current
+        tokensRef.current,
+        catalogue
       );
       if (
         scoped.length > 0 &&
@@ -280,7 +284,7 @@ export function useConstructionCallbacks({
     };
     addToken(suggestion);
     construction.reset();
-  }, [addToken, construction, activeTextSegment, setSegments]);
+  }, [addToken, construction, activeTextSegment, setSegments, catalogue]);
 
   return { handleDropdownItemSelect, finalizeConstruction };
 }
diff --git a/apps/web/src/hooks/useSearchTokens/useSuggestions.ts b/apps/web/src/hooks/useSearchTokens/useSuggestions.ts
index c36ad153..f4ce29d3 100644
--- a/apps/web/src/hooks/useSearchTokens/useSuggestions.ts
+++ b/apps/web/src/hooks/useSearchTokens/useSuggestions.ts
@@ -1,6 +1,9 @@
 import { useMemo } from "react";
 import { getDropdownItems } from "@/components/store-search/facet-construction";
-import { getFilterCatalogueVersion } from "@/components/store-search/filter-catalogue";
+import {
+  type FilterDefinition,
+  getFilterCatalogueVersion,
+} from "@/components/store-search/filter-catalogue";
 import {
   getScopedValueSuggestions,
   getTokenSuggestions,
@@ -24,17 +27,19 @@ export function useSuggestions({
   recategorizeTokenId,
   constructionState,
   blockedCategories,
+  catalogue,
 }: {
   inputValue: string;
   tokens: SearchToken[];
   recategorizeTokenId: string | null;
   constructionState: TokenConstructionState;
   blockedCategories?: FilterCategory[];
+  catalogue?: FilterDefinition[];
 }): {
   suggestions: TokenSuggestion[];
   dropdownItems: DropdownItem[];
 } {
-  const catalogueVersion = getFilterCatalogueVersion();
+  const catalogueVersion = catalogue ? 0 : getFilterCatalogueVersion();
 
   /** Legacy flat suggestions — used for recategorize and auto-tokenize compatibility */
   const suggestions = useMemo(() => {
@@ -49,14 +54,14 @@ export function useSuggestions({
             "",
             token.category,
             otherTokens,
-            undefined,
+            catalogue,
             blockedCategories
           );
         }
         return getTokenSuggestions(
           token.displayText,
           otherTokens,
-          undefined,
+          catalogue,
           blockedCategories
         );
       }
@@ -64,7 +69,7 @@ export function useSuggestions({
     return getTokenSuggestions(
       inputValue,
       tokens,
-      undefined,
+      catalogue,
       blockedCategories
     );
   }, [
@@ -72,6 +77,7 @@ export function useSuggestions({
     tokens,
     recategorizeTokenId,
     catalogueVersion,
+    catalogue,
     blockedCategories,
   ]);
 
@@ -83,7 +89,7 @@ export function useSuggestions({
       inputValue,
       tokens,
       constructionState,
-      undefined,
+      catalogue,
       blockedCategories
     );
   }, [
@@ -92,6 +98,7 @@ export function useSuggestions({
     constructionState,
     recategorizeTokenId,
     catalogueVersion,
+    catalogue,
     blockedCategories,
   ]);
 
diff --git a/apps/web/src/lib/lexical/KeywordNode.tsx b/apps/web/src/lib/lexical/KeywordNode.tsx
deleted file mode 100644
index d2ee8a0f..00000000
--- a/apps/web/src/lib/lexical/KeywordNode.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-import type {
-  DOMConversionMap,
-  DOMConversionOutput,
-  DOMExportOutput,
-  EditorConfig,
-  LexicalNode,
-  NodeKey,
-  SerializedTextNode,
-  Spread,
-} from "lexical";
-import { $applyNodeReplacement, TextNode } from "lexical";
-
-export type KeywordVariant =
-  | "type"
-  | "vendor"
-  | "currency"
-  | "sector"
-  | "theme"
-  | "country"
-  | "aum";
-
-type SerializedKeywordNode = Spread<
-  {
-    variant: KeywordVariant;
-  },
-  SerializedTextNode
->;
-
-const VARIANT_STYLES: Record<
-  KeywordVariant,
-  { bg: string; text: string; border: string }
-> = {
-  type: {
-    bg: "rgba(99,223,21,0.08)",
-    text: "#c2f6a2",
-    border: "#63df15",
-  },
-  vendor: {
-    bg: "rgba(4,114,241,0.08)",
-    text: "#9bc9fd",
-    border: "#0472f1",
-  },
-  currency: {
-    bg: "rgba(118,40,205,0.08)",
-    text: "#caa9ef",
-    border: "#7628cd",
-  },
-  sector: {
-    bg: "rgba(233,12,38,0.08)",
-    text: "#fa9ea9",
-    border: "#e90c26",
-  },
-  theme: {
-    bg: "rgba(237,111,8,0.08)",
-    text: "#fcc79c",
-    border: "#ed6f08",
-  },
-  country: {
-    bg: "rgba(6,239,215,0.08)",
-    text: "#9cfcf3",
-    border: "#06efd7",
-  },
-  aum: {
-    bg: "rgba(24,220,103,0.08)",
-    text: "#a3f5c4",
-    border: "#18dc67",
-  },
-};
-
-/**
- * Custom Lexical node for keyword chips
- * Extends TextNode to allow inline rendering with special styling
- */
-export class KeywordNode extends TextNode {
-  __variant: KeywordVariant;
-
-  static getType(): string {
-    return "keyword";
-  }
-
-  static clone(node: KeywordNode): KeywordNode {
-    return new KeywordNode(node.__text, node.__variant, node.__key);
-  }
-
-  // Constructor with zero required arguments (Lexical best practice for collaboration support)
-  constructor(text?: string, variant?: KeywordVariant, key?: NodeKey) {
-    super(text || "", key);
-    this.__variant = variant || "type";
-  }
-
-  getVariant(): KeywordVariant {
-    return this.getLatest().__variant;
-  }
-
-  setVariant(variant: KeywordVariant): void {
-    const writable = this.getWritable();
-    writable.__variant = variant;
-  }
-
-  createDOM(config: EditorConfig): HTMLElement {
-    const element = document.createElement("span");
-    const styles = VARIANT_STYLES[this.__variant];
-
-    element.className =
-      "inline-flex items-center justify-center gap-[10px] px-[3px] pb-[3px] pt-px font-normal text-[14px] leading-normal whitespace-pre";
-
-    element.style.backgroundColor = styles.bg;
-    element.style.color = styles.text;
-    element.style.borderBottom = `0.5px solid ${styles.border}`;
-
-    element.textContent = this.__text;
-
-    return element;
-  }
-
-  updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
-    const isUpdated = super.updateDOM(prevNode, dom, config);
-
-    if ($isKeywordNode(prevNode) && prevNode.__variant !== this.__variant) {
-      const styles = VARIANT_STYLES[this.__variant];
-      dom.style.backgroundColor = styles.bg;
-      dom.style.color = styles.text;
-      dom.style.borderBottom = `0.5px solid ${styles.border}`;
-    }
-
-    return isUpdated;
-  }
-
-  exportDOM(): DOMExportOutput {
-    const element = document.createElement("span");
-    const styles = VARIANT_STYLES[this.__variant];
-
-    element.className =
-      "inline-flex items-center justify-center gap-[10px] px-2 pb-[3px] pt-px font-normal text-[14px] leading-normal whitespace-pre mx-2";
-
-    element.style.backgroundColor = styles.bg;
-    element.style.color = styles.text;
-    element.style.borderBottom = `0.5px solid ${styles.border}`;
-
-    element.textContent = this.__text;
-
-    return { element };
-  }
-
-  static importDOM(): DOMConversionMap | null {
-    return {
-      span: (node: Node) => ({
-        conversion: convertKeywordElement,
-        priority: 1,
-      }),
-    };
-  }
-
-  static importJSON(serializedNode: SerializedKeywordNode): KeywordNode {
-    const node = $createKeywordNode(
-      serializedNode.text,
-      serializedNode.variant
-    );
-    node.setFormat(serializedNode.format);
-    node.setDetail(serializedNode.detail);
-    node.setMode(serializedNode.mode);
-    node.setStyle(serializedNode.style);
-    return node;
-  }
-
-  exportJSON(): SerializedKeywordNode {
-    return {
-      ...super.exportJSON(),
-      variant: this.__variant,
-      type: "keyword",
-      version: 1,
-    };
-  }
-
-  // Make the node non-editable (user can't edit chip content directly)
-  isSegmented(): boolean {
-    return true;
-  }
-}
-
-function convertKeywordElement(domNode: Node): DOMConversionOutput | null {
-  const element = domNode as HTMLElement;
-  const text = element.textContent;
-  if (text !== null) {
-    const node = $createKeywordNode(text, "type"); // Default variant
-    return { node };
-  }
-  return null;
-}
-
-export function $createKeywordNode(
-  text: string,
-  variant: KeywordVariant
-): KeywordNode {
-  return $applyNodeReplacement(new KeywordNode(text, variant));
-}
-
-function $isKeywordNode(
-  node: LexicalNode | null | undefined
-): node is KeywordNode {
-  return node instanceof KeywordNode;
-}
diff --git a/apps/web/src/lib/lexical/KeywordPlugin.tsx b/apps/web/src/lib/lexical/KeywordPlugin.tsx
deleted file mode 100644
index 93d022c5..00000000
--- a/apps/web/src/lib/lexical/KeywordPlugin.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
-import { registerLexicalTextEntity } from "@lexical/text";
-import type { TextNode } from "lexical";
-import { useEffect } from "react";
-import type { ParsedFiltersWithMatches } from "@/lib/utils/parseAssetSearchQuery";
-import type { KeywordVariant } from "./KeywordNode";
-import { $createKeywordNode, KeywordNode } from "./KeywordNode";
-
-type KeywordPluginProps = {
-  parsedFilters: ParsedFiltersWithMatches | null;
-};
-
-/**
- * Plugin that transforms text into KeywordNodes based on parsed filters
- * Uses Lexical's registerLexicalTextEntity for efficient, automatic text-to-node conversion
- */
-export function KeywordPlugin({ parsedFilters }: KeywordPluginProps) {
-  const [editor] = useLexicalComposerContext();
-
-  useEffect(() => {
-    if (!parsedFilters) {
-      return;
-    }
-
-    // Build a map of all filter matches for quick lookup
-    const matchMap = new Map<
-      string,
-      { filterType: KeywordVariant; matchedText: string }
-    >();
-
-    // Collect all matched texts from all filter types
-    const allMatches: Array<{ text: string; filterType: KeywordVariant }> = [];
-
-    if (parsedFilters.source) {
-      for (const source of parsedFilters.source) {
-        allMatches.push({
-          text: source.matchedText,
-          filterType: "type",
-        });
-        matchMap.set(source.matchedText.toLowerCase(), {
-          filterType: "type",
-          matchedText: source.matchedText,
-        });
-      }
-    }
-
-    if (parsedFilters.issuer) {
-      for (const issuer of parsedFilters.issuer) {
-        allMatches.push({
-          text: issuer.matchedText,
-          filterType: "vendor",
-        });
-        matchMap.set(issuer.matchedText.toLowerCase(), {
-          filterType: "vendor",
-          matchedText: issuer.matchedText,
-        });
-      }
-    }
-
-    if (parsedFilters.listing_currency) {
-      for (const currency of parsedFilters.listing_currency) {
-        allMatches.push({
-          text: currency.matchedText,
-          filterType: "currency",
-        });
-        matchMap.set(currency.matchedText.toLowerCase(), {
-          filterType: "currency",
-          matchedText: currency.matchedText,
-        });
-      }
-    }
-
-    if (parsedFilters.country) {
-      for (const country of parsedFilters.country) {
-        allMatches.push({
-          text: country.matchedText,
-          filterType: "country",
-        });
-        matchMap.set(country.matchedText.toLowerCase(), {
-          filterType: "country",
-          matchedText: country.matchedText,
-        });
-      }
-    }
-
-    if (parsedFilters.aum?.min) {
-      allMatches.push({
-        text: parsedFilters.aum.min.matchedText,
-        filterType: "aum",
-      });
-      matchMap.set(parsedFilters.aum.min.matchedText.toLowerCase(), {
-        filterType: "aum",
-        matchedText: parsedFilters.aum.min.matchedText,
-      });
-    }
-
-    if (parsedFilters.aum?.max) {
-      allMatches.push({
-        text: parsedFilters.aum.max.matchedText,
-        filterType: "aum",
-      });
-      matchMap.set(parsedFilters.aum.max.matchedText.toLowerCase(), {
-        filterType: "aum",
-        matchedText: parsedFilters.aum.max.matchedText,
-      });
-    }
-
-    if (allMatches.length === 0) {
-      return;
-    }
-
-    // Create a regex pattern that matches any of the filter texts
-    // Use lookahead/lookbehind for boundaries to handle special characters like $
-    const escapedPatterns = allMatches.map((match) =>
-      match.text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
-    );
-    // Match at word boundaries OR when preceded/followed by non-alphanumeric characters
-    const pattern = new RegExp(
-      `(^|\\s|[^a-zA-Z0-9])(${escapedPatterns.join("|")})(?=\\s|[^a-zA-Z0-9]|$)`,
-      "gi"
-    );
-
-    // Register text entity transform using Lexical's built-in utility
-    // This automatically handles text → KeywordNode conversion efficiently
-    const unregisterFns = registerLexicalTextEntity(
-      editor,
-      (text) => {
-        // getMatch function: find matches in the text
-        const matches: Array<{ start: number; end: number }> = [];
-        let match;
-
-        // Reset regex lastIndex for global flag
-        pattern.lastIndex = 0;
-
-        while ((match = pattern.exec(text)) !== null) {
-          // match[0] = full match including boundary
-          // match[1] = boundary character before (could be empty for ^)
-          // match[2] = actual matched text
-          const boundaryLength = match[1].length;
-          matches.push({
-            start: match.index + boundaryLength,
-            end: match.index + boundaryLength + match[2].length,
-          });
-        }
-
-        return matches.length > 0 ? matches[0] : null;
-      },
-      KeywordNode,
-      (textNode: TextNode) => {
-        // createNode function: create KeywordNode from matched TextNode
-        const text = textNode.getTextContent();
-        const matchInfo = matchMap.get(text.toLowerCase());
-
-        if (!matchInfo) {
-          // Fallback to default if not found
-          return $createKeywordNode(text, "type");
-        }
-
-        return $createKeywordNode(matchInfo.matchedText, matchInfo.filterType);
-      }
-    );
-
-    // Cleanup function - unregisterFns is an array of cleanup functions
-    return () => {
-      for (const unregister of unregisterFns) {
-        unregister();
-      }
-    };
-  }, [editor, parsedFilters]);
-
-  return null;
-}
diff --git a/package.json b/package.json
index bdb8eabc..2abf64cf 100644
--- a/package.json
+++ b/package.json
@@ -78,8 +78,6 @@
     "@knocklabs/client": "0.14.9",
     "@knocklabs/node": "^1.10.3",
     "@knocklabs/react": "0.7.15",
-    "@lexical/react": "^0.32.1",
-    "@lexical/text": "^0.32.1",
     "@lezer/highlight": "^1.2.3",
     "@mendable/firecrawl-js": "^4.13.2",
     "@next/bundle-analyzer": "16.2.3",
@@ -164,7 +162,6 @@
     "jsdom": "^26.1.0",
     "jsurl2": "^2.2.0",
     "katex": "^0.16.22",
-    "lexical": "^0.32.1",
     "lightweight-charts": "^5.1.0",
     "luxon": "^3.6.1",
     "nanoid": "^5.1.6",
diff --git a/yarn.lock b/yarn.lock
index a6d86c55..b1681d94 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4272,7 +4272,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.6":
+"@floating-ui/react-dom@npm:^2.0.0":
   version: 2.1.6
   resolution: "@floating-ui/react-dom@npm:2.1.6"
   dependencies:
@@ -4284,20 +4284,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@floating-ui/react@npm:^0.27.8":
-  version: 0.27.16
-  resolution: "@floating-ui/react@npm:0.27.16"
-  dependencies:
-    "@floating-ui/react-dom": "npm:^2.1.6"
-    "@floating-ui/utils": "npm:^0.2.10"
-    tabbable: "npm:^6.0.0"
-  peerDependencies:
-    react: ">=17.0.0"
-    react-dom: ">=17.0.0"
-  checksum: 10c0/a026266d8875e69de1ac1e1a00588660c8ee299c1e7d067c5c5fd1d69a46fd10acff5dd6cb66c3fe40a3347b443234309ba95f5b33d49059d0cda121f558f566
-  languageName: node
-  linkType: hard
-
 "@floating-ui/utils@npm:^0.2.10":
   version: 0.2.10
   resolution: "@floating-ui/utils@npm:0.2.10"
@@ -4914,258 +4900,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@lexical/clipboard@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/clipboard@npm:0.32.1"
-  dependencies:
-    "@lexical/html": "npm:0.32.1"
-    "@lexical/list": "npm:0.32.1"
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/679b97ea1a605ef7432808d468b4c9ca49746208074eee8b3018ae27a3b6852643f94263219cde8781ac4cfeb8c896b26c0b9e3a1d027526883b61bb82910c11
-  languageName: node
-  linkType: hard
-
-"@lexical/code@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/code@npm:0.32.1"
-  dependencies:
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-    prismjs: "npm:^1.30.0"
-  checksum: 10c0/4658b03ddd7accd06e97e386b6731d861dc71bf7f84b24d816dcadad43c877f74f012d9435d00aefc7fdedc3bebe5f1e3bf50d5555efd554c855c27e829100e7
-  languageName: node
-  linkType: hard
-
-"@lexical/devtools-core@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/devtools-core@npm:0.32.1"
-  dependencies:
-    "@lexical/html": "npm:0.32.1"
-    "@lexical/link": "npm:0.32.1"
-    "@lexical/mark": "npm:0.32.1"
-    "@lexical/table": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  peerDependencies:
-    react: ">=17.x"
-    react-dom: ">=17.x"
-  checksum: 10c0/6950a19234bc5e7d3ad5cc8831f56b98e0d5ce8df2a5b1734140a5d72cba375b90635f28c7a19bb103a7990e67b0c6dc8aacf474780ae8e672bd94f84ff6131d
-  languageName: node
-  linkType: hard
-
-"@lexical/dragon@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/dragon@npm:0.32.1"
-  dependencies:
-    lexical: "npm:0.32.1"
-  checksum: 10c0/9d1a50d9f0a3be9fe022e65b3b64ed393213a796aaac70ffc5fa77ae112327b248267595e8c06644f0798e24062199a5523d2017367b6bd5dcb61bb332432745
-  languageName: node
-  linkType: hard
-
-"@lexical/hashtag@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/hashtag@npm:0.32.1"
-  dependencies:
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/928d1ccff76392e709491a41c1962298a68d0d8151f861320d7e39de553380892ec63efdd33f80c8b2d6719299143786ea01088b9a56d24bb73b070ad82798a2
-  languageName: node
-  linkType: hard
-
-"@lexical/history@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/history@npm:0.32.1"
-  dependencies:
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/6512db4d9d9e6cae02cd0d8c48c9770c9aa95429fa4a3e6436ae4c4b21046af7d29bfdd1ad036c0ef27442e98bb866ba6243b091464325c056389835a452adb6
-  languageName: node
-  linkType: hard
-
-"@lexical/html@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/html@npm:0.32.1"
-  dependencies:
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/1d6691d8a2694e6230bde714878d9e89b801af9fea305d6cf8b61eaf7edb86c8a3b0f9136e5bb3fe3855dd08d21edbc3ac13c9d70df7c52e559d968c127e64c8
-  languageName: node
-  linkType: hard
-
-"@lexical/link@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/link@npm:0.32.1"
-  dependencies:
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/2ba9d36b18beefd74cb60189ea6ef235eb12b342e4b0019fa77a4f0b54dc3a062e7e36fbdcc77df96c31d4de91e0c2dd603eb2d27314de4447feb14a675b077e
-  languageName: node
-  linkType: hard
-
-"@lexical/list@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/list@npm:0.32.1"
-  dependencies:
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/05346e239af2292c1f14ffd9d0cc43e4f171a284ffd4863c7582f3e5f1c3b4a91a4e491c924a58a426f57a3791a643328f25e5c5cb98fdf5e86f3fc5538e2bdc
-  languageName: node
-  linkType: hard
-
-"@lexical/mark@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/mark@npm:0.32.1"
-  dependencies:
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/d3b64f061acc289cefe77d45672f0d566c04054b0eec6c1380d0646d4ccb4760470d276eaded4d8e90e5ea6074eae6529b597555b9a5115e866c3d01a633c311
-  languageName: node
-  linkType: hard
-
-"@lexical/markdown@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/markdown@npm:0.32.1"
-  dependencies:
-    "@lexical/code": "npm:0.32.1"
-    "@lexical/link": "npm:0.32.1"
-    "@lexical/list": "npm:0.32.1"
-    "@lexical/rich-text": "npm:0.32.1"
-    "@lexical/text": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/04e0c76a7c2ab547ef3b5a978582793b8367354bc846665d84ec986c33392c4edc9c2695803b17d1c76ab56d7b9895be41345368ed3fe5d603ccab98eceb393e
-  languageName: node
-  linkType: hard
-
-"@lexical/offset@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/offset@npm:0.32.1"
-  dependencies:
-    lexical: "npm:0.32.1"
-  checksum: 10c0/c7434a64c1df2bbd3ce9c13777ce1cbf7d75284e401b4197d810d5e7f37fb56a34d37697bbe161b6f6dd900212231044f4a7b65da8152a44fbf86a5acaeb0ddd
-  languageName: node
-  linkType: hard
-
-"@lexical/overflow@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/overflow@npm:0.32.1"
-  dependencies:
-    lexical: "npm:0.32.1"
-  checksum: 10c0/e992525edd392f185b816dbf724a03f4357ae2811eabe4d53668e4815d1a831ab3449a540d4c9c67afc407f6a1602a3cf8eb24c6c9f6908450410a7de7399827
-  languageName: node
-  linkType: hard
-
-"@lexical/plain-text@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/plain-text@npm:0.32.1"
-  dependencies:
-    "@lexical/clipboard": "npm:0.32.1"
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/0b8f9311f0fb492854273d686296f1ec36fad6e95db89cd6911408b66b56f704314b18be9f554811064d74904225833fb11d467f42a41138fcf257a0e9abcd2a
-  languageName: node
-  linkType: hard
-
-"@lexical/react@npm:^0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/react@npm:0.32.1"
-  dependencies:
-    "@floating-ui/react": "npm:^0.27.8"
-    "@lexical/devtools-core": "npm:0.32.1"
-    "@lexical/dragon": "npm:0.32.1"
-    "@lexical/hashtag": "npm:0.32.1"
-    "@lexical/history": "npm:0.32.1"
-    "@lexical/link": "npm:0.32.1"
-    "@lexical/list": "npm:0.32.1"
-    "@lexical/mark": "npm:0.32.1"
-    "@lexical/markdown": "npm:0.32.1"
-    "@lexical/overflow": "npm:0.32.1"
-    "@lexical/plain-text": "npm:0.32.1"
-    "@lexical/rich-text": "npm:0.32.1"
-    "@lexical/table": "npm:0.32.1"
-    "@lexical/text": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    "@lexical/yjs": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-    react-error-boundary: "npm:^3.1.4"
-  peerDependencies:
-    react: ">=17.x"
-    react-dom: ">=17.x"
-  checksum: 10c0/fccc637d31b6151f45895800f7211b8cd0c0300aef94e889a9f1c13cf29fc4cdf25bc921f9fca9f216044d3a83c3d2895f7200cf1507179c8af4aa7ad2a9849f
-  languageName: node
-  linkType: hard
-
-"@lexical/rich-text@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/rich-text@npm:0.32.1"
-  dependencies:
-    "@lexical/clipboard": "npm:0.32.1"
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/921eb7edfd778a5e2d447c20b486fd1c389333a27add1a1ad8cf32c4d28cb8ff016463338c9bfdfa74e0f579e0e5617ec165a07f5701eec0c2ad0b15a5f35046
-  languageName: node
-  linkType: hard
-
-"@lexical/selection@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/selection@npm:0.32.1"
-  dependencies:
-    lexical: "npm:0.32.1"
-  checksum: 10c0/43bb8b1797a95eb085db1f1ecebc4a6dc18c354cd29089bafa2830989a161fe8088c3a035caf8423b102fd2aaf1f32228604b0e4537869f3889a756a9cc9cbb7
-  languageName: node
-  linkType: hard
-
-"@lexical/table@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/table@npm:0.32.1"
-  dependencies:
-    "@lexical/clipboard": "npm:0.32.1"
-    "@lexical/utils": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/5d21f947b81cadc78a962c4c0703d8e771b106e2fd5f3c5f8353608c46d95fccd93e96c3870ffa3521d49e56a46d6936c5cf89b8dc149781aee3676fcddcf3d9
-  languageName: node
-  linkType: hard
-
-"@lexical/text@npm:0.32.1, @lexical/text@npm:^0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/text@npm:0.32.1"
-  dependencies:
-    lexical: "npm:0.32.1"
-  checksum: 10c0/51d8f6208fcebfd5923ed565c61e155b19cdb168fd7c29145f157d0e9ccee9b2ab75e9ba5f82ac7653452affa45dfb1a47db0579643a014febd32f47d14f57e4
-  languageName: node
-  linkType: hard
-
-"@lexical/utils@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/utils@npm:0.32.1"
-  dependencies:
-    "@lexical/list": "npm:0.32.1"
-    "@lexical/selection": "npm:0.32.1"
-    "@lexical/table": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  checksum: 10c0/2a5ec2037bafa022a0f043634b7832c4d5c49704c8aca5c5a5362bf72e3816c6dd19aff01ac2848078fa22af58d8018f05964ddd09ae5cd254d3fd9f269b5f11
-  languageName: node
-  linkType: hard
-
-"@lexical/yjs@npm:0.32.1":
-  version: 0.32.1
-  resolution: "@lexical/yjs@npm:0.32.1"
-  dependencies:
-    "@lexical/offset": "npm:0.32.1"
-    "@lexical/selection": "npm:0.32.1"
-    lexical: "npm:0.32.1"
-  peerDependencies:
-    yjs: ">=13.5.22"
-  checksum: 10c0/62748880f205fb7afa8c4e51325d2deb4c82242093c5280ea83ef8fe3ce6a3652131bb77e7853b356f4933679b18dd1128ee35b5fd0ce195ecc3e4b1224122ac
-  languageName: node
-  linkType: hard
-
 "@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.2.1, @lezer/common@npm:^1.3.0":
   version: 1.3.0
   resolution: "@lezer/common@npm:1.3.0"
@@ -17771,13 +17505,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lexical@npm:0.32.1, lexical@npm:^0.32.1":
-  version: 0.32.1
-  resolution: "lexical@npm:0.32.1"
-  checksum: 10c0/fea1e2aa7f1026aebf209a53dbf149bd4c5ec22a15b5fcef4e38224fb601c9a5fcc9d43801f2e09f375dea919be2e99287d517126849edcb2bc32cb23db04697
-  languageName: node
-  linkType: hard
-
 "lightningcss-android-arm64@npm:1.31.1":
   version: 1.31.1
   resolution: "lightningcss-android-arm64@npm:1.31.1"
@@ -20733,13 +20460,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"prismjs@npm:^1.30.0":
-  version: 1.30.0
-  resolution: "prismjs@npm:1.30.0"
-  checksum: 10c0/f56205bfd58ef71ccfcbcb691fd0eb84adc96c6ff21b0b69fc6fdcf02be42d6ef972ba4aed60466310de3d67733f6a746f89f2fb79c00bf217406d465b3e8f23
-  languageName: node
-  linkType: hard
-
 "proc-log@npm:^5.0.0":
   version: 5.0.0
   resolution: "proc-log@npm:5.0.0"
@@ -21182,17 +20902,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-error-boundary@npm:^3.1.4":
-  version: 3.1.4
-  resolution: "react-error-boundary@npm:3.1.4"
-  dependencies:
-    "@babel/runtime": "npm:^7.12.5"
-  peerDependencies:
-    react: ">=16.13.1"
-  checksum: 10c0/f977ca61823e43de2381d53dd7aa8b4d79ff6a984c9afdc88dc44f9973b99de7fd382d2f0f91f2688e24bb987c0185bf45d0b004f22afaaab0f990a830253bfb
-  languageName: node
-  linkType: hard
-
 "react-hook-form@npm:^7.58.1":
   version: 7.65.0
   resolution: "react-hook-form@npm:7.65.0"
@@ -22940,13 +22649,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tabbable@npm:^6.0.0":
-  version: 6.3.0
-  resolution: "tabbable@npm:6.3.0"
-  checksum: 10c0/57ba019d29b5cfa0c862248883bcec0e6d29d8f156ba52a1f425e7cfeca4a0fc701ab8d035c4c86ddf74ecdbd0e9f454a88d9b55d924a51f444038e9cd14d7a0
-  languageName: node
-  linkType: hard
-
 "tailwind-merge@npm:^3.3.1":
   version: 3.3.1
   resolution: "tailwind-merge@npm:3.3.1"
@@ -23214,8 +22916,6 @@ __metadata:
     "@knocklabs/client": "npm:0.14.9"
     "@knocklabs/node": "npm:^1.10.3"
     "@knocklabs/react": "npm:0.7.15"
-    "@lexical/react": "npm:^0.32.1"
-    "@lexical/text": "npm:^0.32.1"
     "@lezer/highlight": "npm:^1.2.3"
     "@mendable/firecrawl-js": "npm:^4.13.2"
     "@next/bundle-analyzer": "npm:16.2.3"
@@ -23323,7 +23023,6 @@ __metadata:
     jsurl2: "npm:^2.2.0"
     katex: "npm:^0.16.22"
     knip: "npm:^5.61.2"
-    lexical: "npm:^0.32.1"
     lightweight-charts: "npm:^5.1.0"
     lint-staged: "npm:^16.1.2"
     lodash: "npm:^4.18.0"
diff --git a/apps/web/src/components/store-search/TokenAssetSearch.tsx b/apps/web/src/components/store-search/TokenAssetSearch.tsx
new file mode 100644
index 00000000..0bfe9a25
--- /dev/null
+++ b/apps/web/src/components/store-search/TokenAssetSearch.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import type { TokenAssetSearchState } from "@/hooks/useTokenAssetSearch";
+import { TokenSearchBar } from "./TokenSearchBar";
+
+type TokenAssetSearchProps = {
+  autoFocus?: boolean;
+  className?: string;
+  placeholder?: string;
+  searchState: TokenAssetSearchState;
+};
+
+export function TokenAssetSearch({
+  autoFocus,
+  className,
+  placeholder,
+  searchState,
+}: TokenAssetSearchProps): React.ReactElement {
+  return (
+    <TokenSearchBar
+      autoFocus={autoFocus}
+      className={className}
+      isSearching={searchState.isSearching}
+      onSearch={() => {
+        void searchState.executeSearch();
+      }}
+      placeholder={placeholder}
+      tokenState={searchState.tokenState}
+    />
+  );
+}
diff --git a/apps/web/src/hooks/useTokenAssetSearch.ts b/apps/web/src/hooks/useTokenAssetSearch.ts
new file mode 100644
index 00000000..51f88da4
--- /dev/null
+++ b/apps/web/src/hooks/useTokenAssetSearch.ts
@@ -0,0 +1,320 @@
+"use client";
+
+import type { FundsSearchFilters } from "@tilt/sig-api-client/schemas";
+import { useCallback, useEffect, useMemo, useRef } from "react";
+import type { FilterCategory } from "@/components/store-search/types";
+import { useStoreFacetsCatalogue } from "@/components/store-search/use-store-facets-catalogue";
+import { useAssetSearch } from "@/hooks/useAssetSearch";
+import { useSearchTokens } from "@/hooks/useSearchTokens";
+import {
+  type ParsedFiltersWithMatches,
+  parseAssetSearchQuery,
+  SOURCE_TYPES,
+  type SourceType,
+} from "@/lib/utils/parseAssetSearchQuery";
+
+type AssetSearchOptions = NonNullable<Parameters<typeof useAssetSearch>[0]>;
+
+type UseTokenAssetSearchOptions = Omit<
+  AssetSearchOptions,
+  "externalParsedFilters"
+> & {
+  blockedCategories?: FilterCategory[];
+  facetMinCount?: number;
+  facetSources?: FundsSearchFilters["source"];
+  includeTiltSource?: boolean;
+};
+
+export type TokenAssetSearchState = ReturnType<typeof useAssetSearch> & {
+  tokenState: ReturnType<typeof useSearchTokens>;
+};
+
+function canLoadFacetSource(
+  source: SourceType
+): source is Exclude<SourceType, "TILT" | "TOPIC"> {
+  return source !== "TILT" && source !== "TOPIC";
+}
+
+function getFacetSources(
+  allowedSourceTypes: readonly SourceType[] | undefined,
+  facetSources: FundsSearchFilters["source"] | undefined
+): FundsSearchFilters["source"] | undefined {
+  if (facetSources) return facetSources;
+  if (!allowedSourceTypes) return;
+
+  const loadableSources = allowedSourceTypes.filter(canLoadFacetSource);
+  return loadableSources.length > 0
+    ? (loadableSources as FundsSearchFilters["source"])
+    : undefined;
+}
+
+const NON_TOPIC_SOURCE_TYPES = SOURCE_TYPES.filter(
+  (source) => source !== "TOPIC"
+) as SourceType[];
+
+const RANGE_FILTER_KEYS = [
+  "aum",
+  "return_1d",
+  "return_1w",
+  "return_1m",
+  "return_3m",
+  "return_6m",
+  "return_12m",
+  "return_5y",
+  "management_expense_ratio",
+  "management_fee",
+  "top_10_holdings_weight",
+  "holdings_count",
+] as const satisfies readonly (keyof ParsedFiltersWithMatches)[];
+
+const MATCH_ARRAY_FILTER_KEYS = [
+  "source",
+  "issuer",
+  "listing_currency",
+  "country",
+  "geo_exposure_code",
+  "strategy",
+] as const satisfies readonly (keyof ParsedFiltersWithMatches)[];
+
+const LEGACY_FILTER_FILLER_WORDS =
+  /\b(from|with|and|or|the|a|an|in|of|for|to|at|by|AUM)\b/gi;
+
+function mergeMatchArrays<T extends { value: string }>(
+  tokenMatches: readonly T[] | undefined,
+  legacyMatches: readonly T[] | undefined
+): T[] | undefined {
+  const merged: T[] = [];
+  const seen = new Set<string>();
+
+  for (const match of [...(tokenMatches ?? []), ...(legacyMatches ?? [])]) {
+    if (seen.has(match.value)) continue;
+    seen.add(match.value);
+    merged.push(match);
+  }
+
+  return merged.length > 0 ? merged : undefined;
+}
+
+type RangeFilter = NonNullable<ParsedFiltersWithMatches["aum"]>;
+
+function mergeRangeFilters(
+  tokenRange: RangeFilter | undefined,
+  legacyRange: RangeFilter | undefined
+): RangeFilter | undefined {
+  if (!(tokenRange || legacyRange)) return;
+  return {
+    min: tokenRange?.min ?? legacyRange?.min,
+    max: tokenRange?.max ?? legacyRange?.max,
+  };
+}
+
+function mergeSectorConcentrationFilters(
+  tokenSector: ParsedFiltersWithMatches["sector_concentration"],
+  legacySector: ParsedFiltersWithMatches["sector_concentration"]
+): ParsedFiltersWithMatches["sector_concentration"] {
+  if (!(tokenSector || legacySector)) return;
+  const merged: NonNullable<ParsedFiltersWithMatches["sector_concentration"]> =
+    {};
+  for (const key of new Set([
+    ...Object.keys(tokenSector ?? {}),
+    ...Object.keys(legacySector ?? {}),
+  ])) {
+    const range = mergeRangeFilters(tokenSector?.[key], legacySector?.[key]);
+    if (range) merged[key] = range;
+  }
+  return Object.keys(merged).length > 0 ? merged : undefined;
+}
+
+function hasStructuredFilters(filters: ParsedFiltersWithMatches): boolean {
+  return (
+    MATCH_ARRAY_FILTER_KEYS.some((key) => {
+      const value = filters[key];
+      return Array.isArray(value) && value.length > 0;
+    }) ||
+    RANGE_FILTER_KEYS.some((key) => Boolean(filters[key])) ||
+    Boolean(
+      filters.sector_concentration &&
+        Object.keys(filters.sector_concentration).length > 0
+    )
+  );
+}
+
+function stripLegacyFilterFillerWords(query: string): string {
+  return query
+    .replace(LEGACY_FILTER_FILLER_WORDS, "")
+    .replace(/\s+/g, " ")
+    .trim();
+}
+
+function getLegacyAllowedSourceTypes(
+  allowedSourceTypesKey: string
+): readonly SourceType[] {
+  const requestedSources = allowedSourceTypesKey
+    ? (allowedSourceTypesKey.split(",") as SourceType[])
+    : [...SOURCE_TYPES];
+  const nonTopicSources = requestedSources.filter(
+    (source) => source !== "TOPIC"
+  );
+  return nonTopicSources.length > 0 ? nonTopicSources : NON_TOPIC_SOURCE_TYPES;
+}
+
+export function mergeTokenAssetSearchFilters(
+  tokenFilters: ParsedFiltersWithMatches | null,
+  allowedSourceTypes: readonly SourceType[]
+): ParsedFiltersWithMatches | null {
+  if (!tokenFilters) return null;
+
+  const legacyFilters = tokenFilters.cleanedQuery.trim()
+    ? parseAssetSearchQuery(tokenFilters.cleanedQuery, allowedSourceTypes)
+    : ({ cleanedQuery: "" } satisfies ParsedFiltersWithMatches);
+
+  const merged: ParsedFiltersWithMatches = {
+    cleanedQuery: legacyFilters.cleanedQuery,
+    source: mergeMatchArrays(tokenFilters.source, legacyFilters.source),
+    issuer: mergeMatchArrays(tokenFilters.issuer, legacyFilters.issuer),
+    listing_currency: mergeMatchArrays(
+      tokenFilters.listing_currency,
+      legacyFilters.listing_currency
+    ),
+    country: mergeMatchArrays(tokenFilters.country, legacyFilters.country),
+    geo_exposure_code: mergeMatchArrays(
+      tokenFilters.geo_exposure_code,
+      legacyFilters.geo_exposure_code
+    ),
+    strategy: mergeMatchArrays(tokenFilters.strategy, legacyFilters.strategy),
+    aum: mergeRangeFilters(tokenFilters.aum, legacyFilters.aum),
+    return_1d: mergeRangeFilters(
+      tokenFilters.return_1d,
+      legacyFilters.return_1d
+    ),
+    return_1w: mergeRangeFilters(
+      tokenFilters.return_1w,
+      legacyFilters.return_1w
+    ),
+    return_1m: mergeRangeFilters(
+      tokenFilters.return_1m,
+      legacyFilters.return_1m
+    ),
+    return_3m: mergeRangeFilters(
+      tokenFilters.return_3m,
+      legacyFilters.return_3m
+    ),
+    return_6m: mergeRangeFilters(
+      tokenFilters.return_6m,
+      legacyFilters.return_6m
+    ),
+    return_12m: mergeRangeFilters(
+      tokenFilters.return_12m,
+      legacyFilters.return_12m
+    ),
+    return_5y: mergeRangeFilters(
+      tokenFilters.return_5y,
+      legacyFilters.return_5y
+    ),
+    management_expense_ratio: mergeRangeFilters(
+      tokenFilters.management_expense_ratio,
+      legacyFilters.management_expense_ratio
+    ),
+    management_fee: mergeRangeFilters(
+      tokenFilters.management_fee,
+      legacyFilters.management_fee
+    ),
+    top_10_holdings_weight: mergeRangeFilters(
+      tokenFilters.top_10_holdings_weight,
+      legacyFilters.top_10_holdings_weight
+    ),
+    holdings_count: mergeRangeFilters(
+      tokenFilters.holdings_count,
+      legacyFilters.holdings_count
+    ),
+    sector_concentration: mergeSectorConcentrationFilters(
+      tokenFilters.sector_concentration,
+      legacyFilters.sector_concentration
+    ),
+  };
+
+  if (hasStructuredFilters(merged)) {
+    merged.cleanedQuery = stripLegacyFilterFillerWords(merged.cleanedQuery);
+  }
+
+  return merged;
+}
+
+export function useTokenAssetSearch(
+  options: UseTokenAssetSearchOptions = {}
+): TokenAssetSearchState {
+  const {
+    blockedCategories,
+    facetMinCount = 1,
+    facetSources,
+    includeTiltSource,
+    ...assetSearchOptions
+  } = options;
+  const sources = getFacetSources(
+    assetSearchOptions.allowedSourceTypes,
+    facetSources
+  );
+  const allowedSourceTypesKey = useMemo(
+    () =>
+      [...(assetSearchOptions.allowedSourceTypes ?? SOURCE_TYPES)]
+        .sort()
+        .join(","),
+    [assetSearchOptions.allowedSourceTypes]
+  );
+  const legacyAllowedSourceTypes = useMemo(
+    () => getLegacyAllowedSourceTypes(allowedSourceTypesKey),
+    [allowedSourceTypesKey]
+  );
+  const shouldIncludeTiltSource =
+    includeTiltSource ??
+    assetSearchOptions.allowedSourceTypes?.includes("TILT") ??
+    true;
+
+  const facetsQuery = useStoreFacetsCatalogue({
+    includeTiltSource: shouldIncludeTiltSource,
+    minCount: facetMinCount,
+    registerGlobal: false,
+    sources,
+  });
+  const tokenState = useSearchTokens(undefined, {
+    blockedCategories,
+    catalogue: facetsQuery.catalogue,
+  });
+  const displayQuery = useMemo(
+    () => tokenState.toDisplayQuery(),
+    [tokenState.toDisplayQuery]
+  );
+  const parsedFilters = useMemo(
+    () =>
+      mergeTokenAssetSearchFilters(
+        tokenState.toParsedFilters(),
+        legacyAllowedSourceTypes
+      ),
+    [tokenState.toParsedFilters, legacyAllowedSourceTypes]
+  );
+
+  const assetSearch = useAssetSearch({
+    ...assetSearchOptions,
+    externalParsedFilters: parsedFilters,
+  });
+  const lastSearchRef = useRef("");
+  const { setQuery } = assetSearch;
+
+  useEffect(() => {
+    if (displayQuery === lastSearchRef.current) return;
+    lastSearchRef.current = displayQuery;
+    setQuery(displayQuery);
+  }, [displayQuery, setQuery]);
+
+  const clearResults = useCallback(() => {
+    tokenState.clearAll();
+    assetSearch.clearResults();
+    lastSearchRef.current = "";
+  }, [assetSearch.clearResults, tokenState.clearAll]);
+
+  return {
+    ...assetSearch,
+    clearResults,
+    tokenState,
+  };
+}
diff --git a/apps/web/vitest/use-search-tokens-catalogue.test.tsx b/apps/web/vitest/use-search-tokens-catalogue.test.tsx
new file mode 100644
index 00000000..c82b48fc
--- /dev/null
+++ b/apps/web/vitest/use-search-tokens-catalogue.test.tsx
@@ -0,0 +1,74 @@
+import { act, renderHook } from "@testing-library/react";
+import { afterEach, describe, expect, it } from "vitest";
+import type { FilterDefinition } from "@/components/store-search/filter-catalogue";
+import {
+  resetFilterCatalogue,
+  setFilterCatalogue,
+} from "@/components/store-search/filter-catalogue";
+import { CATEGORY_TO_VARIANT } from "@/components/store-search/types";
+import { useSearchTokens } from "@/hooks/useSearchTokens";
+
+function sourceCatalogue(
+  values: Array<{
+    aliases: string[];
+    canonical: string;
+    label: string;
+  }>
+): FilterDefinition[] {
+  return [
+    {
+      category: "source",
+      values,
+      variant: CATEGORY_TO_VARIANT.source,
+    },
+  ];
+}
+
+describe("useSearchTokens catalogue scoping", () => {
+  afterEach(() => {
+    resetFilterCatalogue();
+  });
+
+  it("uses its own catalogue when another mounted search changes the shared singleton", () => {
+    const indexCatalogue = sourceCatalogue([
+      {
+        aliases: ["index", "indices"],
+        canonical: "TILT_INDEX|EXTERNAL_INDEX",
+        label: "Index",
+      },
+    ]);
+    const etfCatalogue = sourceCatalogue([
+      {
+        aliases: ["etf", "etfs"],
+        canonical: "ETF",
+        label: "ETF",
+      },
+    ]);
+
+    const { result: indexSearch } = renderHook(() =>
+      useSearchTokens(undefined, { catalogue: indexCatalogue })
+    );
+    const { result: etfSearch } = renderHook(() =>
+      useSearchTokens(undefined, { catalogue: etfCatalogue })
+    );
+
+    setFilterCatalogue(etfCatalogue);
+
+    act(() => {
+      indexSearch.current.setInputValue("index");
+      etfSearch.current.setInputValue("etf");
+    });
+
+    expect(indexSearch.current.suggestions).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ displayText: "Index" }),
+      ])
+    );
+    expect(indexSearch.current.suggestions).not.toEqual(
+      expect.arrayContaining([expect.objectContaining({ displayText: "ETF" })])
+    );
+    expect(etfSearch.current.suggestions).toEqual(
+      expect.arrayContaining([expect.objectContaining({ displayText: "ETF" })])
+    );
+  });
+});
diff --git a/apps/web/vitest/use-store-facets-catalogue.test.tsx b/apps/web/vitest/use-store-facets-catalogue.test.tsx
new file mode 100644
index 00000000..deba67c8
--- /dev/null
+++ b/apps/web/vitest/use-store-facets-catalogue.test.tsx
@@ -0,0 +1,121 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { renderHook, waitFor } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import {
+  type FilterDefinition,
+  getFilterCatalogue,
+  resetFilterCatalogue,
+  setFilterCatalogue,
+} from "@/components/store-search/filter-catalogue";
+import { CATEGORY_TO_VARIANT } from "@/components/store-search/types";
+import { useStoreFacetsCatalogue } from "@/components/store-search/use-store-facets-catalogue";
+
+function sourceCatalogue(
+  values: Array<{
+    aliases: string[];
+    canonical: string;
+    label: string;
+  }>
+): FilterDefinition[] {
+  return [
+    {
+      category: "source",
+      values,
+      variant: CATEGORY_TO_VARIANT.source,
+    },
+  ];
+}
+
+function createWrapper() {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+  return function Wrapper({ children }: { children: ReactNode }) {
+    return (
+      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
+    );
+  };
+}
+
+function sourceLabels(catalogue: FilterDefinition[]): string[] {
+  return (
+    catalogue
+      .find((definition) => definition.category === "source")
+      ?.values.map((value) => value.label) ?? []
+  );
+}
+
+describe("useStoreFacetsCatalogue", () => {
+  beforeEach(() => {
+    window.localStorage.clear();
+  });
+
+  afterEach(() => {
+    resetFilterCatalogue();
+    vi.unstubAllGlobals();
+  });
+
+  it("does not overwrite the shared catalogue when global registration is disabled", async () => {
+    const globalCatalogue = sourceCatalogue([
+      {
+        aliases: ["etf", "etfs"],
+        canonical: "ETF",
+        label: "ETF",
+      },
+    ]);
+    setFilterCatalogue(globalCatalogue);
+
+    const fetchMock = vi.fn(
+      async () =>
+        new Response(
+          JSON.stringify({
+            counts: {
+              country: [],
+              fund_currency: [],
+              geo_exposure_code: [],
+              issuer: [],
+              source: [{ count: 1, value: "TILT_INDEX" }],
+              strategy: [],
+            },
+            facets: {
+              country: [],
+              fund_currency: [],
+              geo_exposure_code: [],
+              issuer: [],
+              source: ["TILT_INDEX"],
+              strategy: [],
+            },
+            meta: {
+              cache_ttl_seconds: 300,
+              generated_at: "2026-05-28T00:00:00.000Z",
+              min_count: 1,
+              sources: ["TILT_INDEX"],
+            },
+          }),
+          { status: 200 }
+        )
+    );
+    vi.stubGlobal("fetch", fetchMock);
+
+    const { result } = renderHook(
+      () =>
+        useStoreFacetsCatalogue({
+          includeTiltSource: false,
+          registerGlobal: false,
+          sources: ["TILT_INDEX"],
+        }),
+      { wrapper: createWrapper() }
+    );
+
+    await waitFor(() => expect(result.current.isSuccess).toBe(true));
+
+    expect(sourceLabels(result.current.catalogue)).toContain("Index");
+    expect(sourceLabels(getFilterCatalogue())).toEqual(["ETF"]);
+  });
+});
diff --git a/apps/web/vitest/use-token-asset-search-filters.test.ts b/apps/web/vitest/use-token-asset-search-filters.test.ts
new file mode 100644
index 00000000..1c395208
--- /dev/null
+++ b/apps/web/vitest/use-token-asset-search-filters.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, it } from "vitest";
+import { mergeTokenAssetSearchFilters } from "@/hooks/useTokenAssetSearch";
+import type { ParsedFiltersWithMatches } from "@/lib/utils/parseAssetSearchQuery";
+
+describe("mergeTokenAssetSearchFilters", () => {
+  it("preserves legacy natural-language filters from free text", () => {
+    const merged = mergeTokenAssetSearchFilters(
+      {
+        cleanedQuery: "ETF with AUM under $500M",
+      },
+      ["ETF", "TILT_INDEX"]
+    );
+
+    expect(merged).toMatchObject({
+      aum: {
+        max: { value: "500000000", matchedText: "under $500M" },
+      },
+      cleanedQuery: "",
+      source: [{ value: "ETF", matchedText: "ETF" }],
+    });
+  });
+
+  it("merges explicit token filters with legacy-parsed free text", () => {
+    const tokenFilters: ParsedFiltersWithMatches = {
+      cleanedQuery: "with AUM under $500M",
+      source: [{ value: "ETF", matchedText: "ETF" }],
+    };
+
+    const merged = mergeTokenAssetSearchFilters(tokenFilters, [
+      "ETF",
+      "TILT_INDEX",
+    ]);
+
+    expect(merged).toMatchObject({
+      aum: {
+        max: { value: "500000000", matchedText: "under $500M" },
+      },
+      cleanedQuery: "",
+      source: [{ value: "ETF", matchedText: "ETF" }],
+    });
+  });
+});
