import { useState, useRef, useEffect } from 'react' import { useNavigate, useSearchParams, useLocation } from 'react-router-dom' import { useQuery, keepPreviousData } from '@tanstack/react-query' import api from '../lib/api' import { fetchTopics, type TopicSummary } from '../api/topics' import { fetchPolicyQuestions, type PolicyQuestionSummary } from '../api/policyQuestions' import { withSpan } from '../instrumentation' import { ArrowLeftIcon, MagnifyingGlassIcon, UserIcon, CalendarIcon, BuildingOfficeIcon, HeartIcon, XMarkIcon, AdjustmentsHorizontalIcon, CheckIcon, MapPinIcon, ChevronDownIcon, ChevronUpIcon, GlobeAltIcon, VideoCameraIcon, DocumentTextIcon, ChatBubbleBottomCenterTextIcon, ScaleIcon, BanknotesIcon, MegaphoneIcon, DocumentMagnifyingGlassIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline' import { formatCurrency, formatCityState, titleCaseCity, expandStateName } from '../utils/formatters' import MeetingThumbnail from '../components/MeetingThumbnail' import { StoryCard, CONTESTED_LENS, TRANSCRIPT_LENS, type RenderCard } from '../components/StoryLenses' import { parseSpeaker } from '../lib/speakers' import { getLaunchCounty } from '../lib/launchCounties' import { DocumentViewerProvider, useDocumentViewer } from '../components/DocumentViewerContext' type SearchResultType = | 'leader' | 'person' | 'meeting' | 'organization' | 'cause' | 'bill' | 'topic' | 'decision' | 'document' | 'meeting_document' | 'grant' | 'grant_opportunity' | 'question' interface SearchResult { type: SearchResultType result_type?: SearchResultType title: string subtitle: string description: string // Optional: a result with no stable detail key (e.g. an MDM person with a // null person_uid) comes back with no url and renders as non-clickable. url?: string | null score: number // Loosely typed grab-bag, but a few keys are contractually meaningful: // - analysis_pending (meeting results): true ⇒ an UNANALYZED meeting shown // because its raw transcript is available. Such results have NO // event_meeting_id, carry an EXTERNAL YouTube `url`, and put a transcript // snippet (possibly with highlights) in `description`. // false/absent ⇒ an AI-analyzed meeting that deep-links to /meetings/{id}. // - video_id (meeting results): YouTube video id for unanalyzed meetings. metadata: Record & { analysis_pending?: boolean video_id?: string event_meeting_id?: number | string meeting_id?: number | string // Raw AI speaker slugs for a decision result; parsed client-side into the // card's "Voices" attribution row. Absent when the result has no testimony. speakers?: string[] } } interface SearchResponse { query: string total_results: number type_totals?: { leaders?: number persons?: number meetings: number organizations: number causes: number bills: number topics: number decisions: number jurisdictions: number documents?: number meeting_documents?: number grants?: number grant_opportunities?: number questions?: number } results: { leaders?: SearchResult[] persons?: SearchResult[] meetings: SearchResult[] organizations: SearchResult[] causes: SearchResult[] bills: SearchResult[] topics: SearchResult[] decisions: SearchResult[] jurisdictions?: SearchResult[] documents?: SearchResult[] meeting_documents?: SearchResult[] grants?: SearchResult[] grant_opportunities?: SearchResult[] questions?: SearchResult[] } pagination: { page: number limit: number offset: number total_pages: number has_next: boolean has_prev: boolean } filters: { state?: string ntee_code?: string types: string[] } } // Map legacy request type names to the current /api/search vocabulary so old // links (?types=contacts / ?types=people / ?types=person) still resolve. function normalizeTypeAlias(t: string): string { if (t === 'contacts' || t === 'leader') return 'leaders' if (t === 'people' || t === 'person') return 'persons' return t } // The result-type vocabulary, shared by the URL whitelist and the Advanced // Filters "Result types" checkbox group. Previously the pills were a separate // inline array in the filter bar; that confusing pill row was removed in favor // of a single source of truth surfaced inside the flyout. const RESULT_TYPES = [ { type: 'leaders', label: 'Leaders' }, // 'persons' (non-government people from mdm_person) is intentionally omitted: // the backend gates that category off (PERSONS_SEARCH_ENABLED) to stay within // Neon free-tier storage. Government leaders remain searchable via 'leaders'. // Re-add this entry if the backend flag is flipped back on. { type: 'organizations', label: 'Organizations' }, { type: 'causes', label: 'Causes' }, { type: 'meetings', label: 'Meetings' }, // 'documents' = meeting transcripts (full-text searched). Most fluoride/ // policy-term hits live in the transcript body, not in meeting titles or // extracted decisions, so this is often the only category that surfaces them. { type: 'documents', label: 'Videos' }, // 'meeting_documents' = official meeting PDFs (agenda / minutes / attachments) // from public.event_meeting_document, full-text searched over extracted PDF // content where present. Distinct from 'documents' (transcripts) above. { type: 'meeting_documents', label: 'Meeting Documents' }, { type: 'bills', label: 'Bills' }, { type: 'topics', label: 'Topics' }, { type: 'decisions', label: 'Decisions' }, { type: 'grants', label: 'Grants' }, { type: 'grant_opportunities', label: 'Grant Opportunities' }, // Cross-jurisdiction policy questions (the "big questions" registry). { type: 'questions', label: 'Questions' }, ] as const const ALL_RESULT_TYPE_KEYS = RESULT_TYPES.map((t) => t.type) as readonly string[] // Default selection when no ?types= is present — every result type is on by default. const DEFAULT_RESULT_TYPES = [...ALL_RESULT_TYPE_KEYS] // Tab order for the results view. Each result type that returns hits becomes a // tab; only the active tab's section renders. Decisions lead because the Money // Moves / civic-decision lenses are the primary reason users reach search; the // remaining tabs follow the long-standing section order. `key` matches the // keys of SearchResponse.results / type_totals so counts resolve directly. const RESULT_TABS = [ { key: 'decisions', label: 'Decisions' }, { key: 'leaders', label: 'Leaders' }, { key: 'persons', label: 'People' }, { key: 'organizations', label: 'Organizations' }, { key: 'causes', label: 'Causes' }, { key: 'meetings', label: 'Meetings' }, { key: 'documents', label: 'Videos' }, { key: 'meeting_documents', label: 'Meeting Documents' }, { key: 'bills', label: 'Bills' }, { key: 'topics', label: 'Topics' }, { key: 'questions', label: 'Questions' }, { key: 'grants', label: 'Grants' }, { key: 'grant_opportunities', label: 'Grant Opportunities' }, { key: 'jurisdictions', label: 'Jurisdictions' }, ] as const // Advanced "Cause" filter options — the EveryOrg cause taxonomy. The `id` is the // cause slug the API's CAUSE_KEYWORDS map / ?cause_id= expects; KEEP IN SYNC with // api/routes/search.py CAUSE_KEYWORDS. A cause narrows decisions/meetings/topics // to its keyword set, matched through child decisions then transcript content. const CAUSE_OPTIONS: { id: string; label: string }[] = [ { id: 'animals', label: 'Animals' }, { id: 'arts', label: 'Arts & Culture' }, { id: 'climate', label: 'Climate' }, { id: 'disasters', label: 'Disaster Relief' }, { id: 'education', label: 'Education' }, { id: 'environment', label: 'Environment' }, { id: 'foodbanks', label: 'Food & Hunger' }, { id: 'health', label: 'Health' }, { id: 'humanitarian', label: 'Humanitarian' }, { id: 'justice', label: 'Justice & Equity' }, { id: 'lgbt', label: 'LGBTQ' }, { id: 'mental-health', label: 'Mental Health' }, { id: 'religion', label: 'Religion' }, { id: 'seniors', label: 'Seniors' }, { id: 'water', label: 'Water' }, { id: 'women', label: 'Women' }, { id: 'youth', label: 'Youth' }, ] // Stable dot color for a cause/NTEE string (small palette, hashed by key). const CAUSE_DOT_PALETTE = [ '#2F5D62', // teal '#3B82F6', // blue '#8B5CF6', // violet '#EC4899', // pink '#F59E0B', // amber '#10B981', // emerald '#EF4444', // red '#6366F1', // indigo ] function causeDotColor(key: string): string { let hash = 0 for (let i = 0; i < key.length; i++) { hash = (hash * 31 + key.charCodeAt(i)) | 0 } return CAUSE_DOT_PALETTE[Math.abs(hash) % CAUSE_DOT_PALETTE.length] } // Format a 9-digit IRS EIN as "XX-XXXXXXX"; leave anything else untouched. function formatEin(ein: string): string { const digits = ein.replace(/\D/g, '') return digits.length === 9 ? `${digits.slice(0, 2)}-${digits.slice(2)}` : ein } // Searchable Topic picker — replaces a long native setQuery(e.target.value)} placeholder="Search topics…" className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-9 pr-3 text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary-500" /> ) : ( )} {open && ( )} ) } // Render a ts_headline snippet (matched passage wrapped in literal // markers) as highlighted React nodes WITHOUT // dangerouslySetInnerHTML: split on the markers and wrap the odd segments in // , so every text segment is React-escaped and the raw transcript body // can never inject markup. Shared by the transcript tiles + the list card. function highlightSnippet(text: string) { return text.split(/<\/?mark>/).map((seg, i) => i % 2 === 1 ? ( {seg} ) : ( {seg} ), ) } function UnifiedSearchInner() { const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const routerLocation = useLocation() // In-app PDF popout (same react-pdf modal used on the Meeting Documents and // Decision Detail pages). Available because UnifiedSearch is wrapped in a // DocumentViewerProvider; meeting-document tiles open the PDF here instead of // a raw new tab. const docViewer = useDocumentViewer() // Whether the user arrived here from the home page (which has no sidebar/nav // to get back). The Home search navigations tag the route with // `state.fromHome`; we capture it ONCE on mount because subsequent // setSearchParams calls (filters/pagination) drop location.state, which would // otherwise make the back button vanish as soon as the user interacts. const [cameFromHome] = useState( () => Boolean((routerLocation.state as { fromHome?: boolean } | null)?.fromHome), ) // Navigate to a result's detail page, but only when it has a url. Results // without a stable detail key (url == null) are rendered non-clickable, so // this is a no-op for them instead of routing to a 404. // // Some result types (e.g. opportunities) carry an EXTERNAL url // (https://www.grants.gov/...) that has no matching App.tsx route. Those must // open in a new tab instead of being handed to react-router (which would 404). const isExternalUrl = (url?: string | null): boolean => !!url && /^https?:\/\//i.test(url) const openResult = (url?: string | null) => { if (!url) return if (isExternalUrl(url)) { window.open(url, '_blank', 'noopener,noreferrer') } else { navigate(url) } } // Initialize state directly from URL params (lazy initializer for performance) const [query, setQuery] = useState(() => searchParams.get('q') || '') const [activeQuery, setActiveQuery] = useState(() => searchParams.get('q') || '') const [selectedEin, setSelectedEin] = useState(() => searchParams.get('ein') || '') const [selectedTypes, setSelectedTypes] = useState(() => { const typesParam = searchParams.get('types') if (typesParam) { const types = typesParam.split(',').map(t => t.trim()).map(normalizeTypeAlias).filter(t => ALL_RESULT_TYPE_KEYS.includes(t) ) return types.length > 0 ? types : [...DEFAULT_RESULT_TYPES] } return [...DEFAULT_RESULT_TYPES] }) const [selectedState, setSelectedState] = useState(() => searchParams.get('state') || '') // Meeting-document type filter ('' = all; 'agenda' | 'minutes' | 'attachment'). // Gated on the 'meeting_documents' result type; URL-synced like selectedState. const [selectedDocType, setSelectedDocType] = useState(() => searchParams.get('document_type') || '') const [currentPage, setCurrentPage] = useState(() => parseInt(searchParams.get('page') || '1')) const [showFilters, setShowFilters] = useState(false) // Which results tab is active. Seeded from ?tab= for shareable deep links; // the render-time `effectiveTab` falls back to the first available tab // (Decisions when present) so a stale/unavailable selection never blanks the // results. const [activeTab, setActiveTab] = useState(() => searchParams.get('tab') || '') const [showSuggestions, setShowSuggestions] = useState(false) const [sortBy, setSortBy] = useState(() => searchParams.get('sort') || 'relevance') const [nteeCategory, setNteeCategory] = useState(() => searchParams.get('ntee') || '') // Advanced "Topic" (named civic-topic catalog) + "Question" (policy-question // registry) filters. Held as the raw id string the { setQuery(e.target.value) setShowSuggestions(true) }} onFocus={() => setShowSuggestions(true)} placeholder="Search people, meetings, organizations, bills, topics, decisions, causes..." className="w-full pl-4 pr-12 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-lg text-gray-900" /> {query && ( )} {/* Rich Preview Dropdown with Grouped Results */} {showSuggestions && query.length >= 2 && (
{/* Loading State */} {(isFetchingPreview || query !== debouncedQuery) && (

Searching...

)} {/* Results */} {!isFetchingPreview && query === debouncedQuery && previewResults && previewResults.total_results > 0 && ( <> {/* Meetings Section */} {previewResults.results.meetings && previewResults.results.meetings.length > 0 && (
Meetings
{previewResults.results.meetings.slice(0, 3).map((result, idx) => ( ))}
)} {/* Decisions Section */} {previewResults.results.decisions && previewResults.results.decisions.length > 0 && (
Decisions
{previewResults.results.decisions.slice(0, 3).map((result, idx) => ( ))}
)} {/* Causes Section */} {previewResults.results.causes.length > 0 && (
Causes
{previewResults.results.causes.length > 0 && ( )}
{previewResults.results.causes.slice(0, 3).map((result, idx) => ( ))}
)} {/* Leaders Section (government officials) */} {(previewResults.results.leaders ?? []).length > 0 && (
Leaders
{(previewResults.results.leaders ?? []).slice(0, 3).map((result, idx) => ( ))}
)} {/* People Section (real people incl. residents/homeowners) */} {(previewResults.results.persons ?? []).length > 0 && (
People
{(previewResults.results.persons ?? []).slice(0, 3).map((result, idx) => ( ))}
)} {/* Organizations Section */} {previewResults.results.organizations.length > 0 && (
Organizations
{previewResults.results.organizations.length > 0 && ( )}
{previewResults.results.organizations.slice(0, 3).map((result, idx) => ( ))}
)} {/* Bills Section */} {previewResults.results.bills && previewResults.results.bills.length > 0 && (
Bills
{previewResults.results.bills.length > 0 && ( )}
{previewResults.results.bills.slice(0, 3).map((result, idx) => ( ))}
)} {/* Topics Section */} {previewResults.results.topics && previewResults.results.topics.length > 0 && (
Topics
{previewResults.results.topics.length > 0 && ( )}
{previewResults.results.topics.slice(0, 3).map((result, idx) => ( ))}
)} {/* Questions Section (cross-jurisdiction policy questions) */} {previewResults.results.questions && previewResults.results.questions.length > 0 && (
Questions
{previewResults.results.questions.slice(0, 3).map((result, idx) => ( ))}
)} {/* Footer with total results */}
)} {/* No Results State */} {!isFetchingPreview && query === debouncedQuery && previewResults && previewResults.total_results === 0 && (

No results found for "{query}"

Try a different search term

)}
)} {/* Filter Bar — a single entry point into Advanced Filters, kept on the same row as the search box. The old type-selection pill row lived here; it was confusing, so result-type selection now lives inside the flyout alongside the other filters. */} {/* Active Filters Display */} {(selectedState || selectedCity || sortBy !== 'relevance' || nteeCategory || selectedTopicId || selectedQuestionId || jurisdictionDetails.length > 0 || includeFullText) && (
Active filters: {selectedState && ( State: {selectedState} )} {/* Place chip — reflects the ACTIVE scope: the county when broadened ("Include surrounding county" on), else the city. Label and data move in lockstep (MANDATORY scope-label rule). */} {selectedCity && ( {countyScopeOn ? `County: ${placeScopeLabel}` : `City: ${selectedCity}`} )} {/* Include-surrounding-county toggle — only when the active city is a known launch city with a distinct county (SF excluded). Default ON via the absent `county` param. */} {countyBroadenAvailable && launchCounty && ( )} {jurisdictionDetails.length > 0 && ( {jurisdictionDetails.length} Jurisdictions )} {sortBy !== 'relevance' && ( Sort: { sortBy === 'name-asc' ? 'Name A-Z' : sortBy === 'name-desc' ? 'Name Z-A' : sortBy === 'revenue-desc' ? 'Revenue ↓' : sortBy === 'revenue-asc' ? 'Revenue ↑' : sortBy === 'assets-desc' ? 'Assets ↓' : sortBy === 'assets-asc' ? 'Assets ↑' : sortBy } )} {nteeCategory && ( Category: {nteeCategory} )} {selectedTopicId && ( Topic: {(topicOptions ?? []).find((t) => String(t.topic_id) === selectedTopicId)?.name ?? selectedTopicId} )} {selectedCauseId && ( Cause: {CAUSE_OPTIONS.find((c) => c.id === selectedCauseId)?.label ?? selectedCauseId} )} {selectedQuestionId && ( Question: {(questionOptions ?? []).find((qn) => qn.question_id === selectedQuestionId)?.canonical_text ?? selectedQuestionId} )} {includeFullText && ( Full text )}
)} {/* Advanced Filters Flyout */} {showFilters && ( <> {/* Backdrop */}
setShowFilters(false)} /> {/* Flyout Sidebar */}
{/* Header */}

Advanced Filters

{/* Filters */}
{/* Result Types — replaces the old pill row. Pick which kinds of results to include. */}
{RESULT_TYPES.map(({ type, label }) => ( ))}
{/* State Filter */}
{/* Sort By */}
{/* Cause / NTEE category (for organizations) */}
{/* Document type (for Meeting Documents). Narrows the official meeting-document results to agendas, minutes, or attachments. Gated on the 'meeting_documents' result type being selected. */}

Filters Meeting Documents by kind.

{/* Topic — named civic-topic catalog (/api/topics). Narrows decisions / meetings / topics to the chosen topic's keyword set. */}
{ setSelectedTopicId(id) setCurrentPage(1) setTimeout(() => handleSearch(), 0) }} />

Limits decisions, meetings & topics to this civic topic.

{/* Cause — EveryOrg cause taxonomy. Narrows decisions/meetings/ topics to the cause's keyword set, matched through child decisions then transcript content (cause -> decision -> meeting, transcript fallback). ?cause_id= the slug. */}

Limits decisions, meetings & topics to this cause.

{/* Question — policy-question registry (/api/policy-question/). Narrows decisions to those that instantiate the chosen question. */}

Limits decisions to those deciding this policy question.

{/* Full Text Search Checkbox */}
{/* Footer Actions */}
)}
{/* Search Results */} {(activeQuery || selectedState || searchResults || tabCountsData) && (
{(isSearching || isCountsLoading) && (

Searching...

)} {error && (

Error loading search results. Please try again.

)} {/* No matches at all (counts came back empty). Per-tab empties can't happen — a tab only exists when its count is > 0. */} {!isCountsLoading && tabCountsData && tabCountsData.total_results === 0 && (

No results found

Try different keywords or adjust your filters

)} {searchResults && searchResults.total_results !== undefined && searchResults.pagination && ( <> {/* Results Summary — headline is the GRAND total across all tabs (from the counts call); the sub-line describes the active tab's current page (pagination is per-tab). */}

{(tabCountsData?.total_results ?? searchResults.total_results).toLocaleString()} results {searchResults.query ? ` for "${searchResults.query}"` : ''}

{searchResults.total_results > 0 && (

Showing {searchResults.pagination.offset + 1}– {Math.min(searchResults.pagination.offset + searchResults.pagination.limit, searchResults.total_results)} {' '}of {searchResults.total_results.toLocaleString()}{' '} {(RESULT_TABS.find((t) => t.key === effectiveTab)?.label ?? effectiveTab).toLowerCase()} {selectedState && ` · State: ${selectedState}`} {selectedCity && (countyScopeOn ? ` · County: ${placeScopeLabel}` : ` · City: ${selectedCity}`)}

)}
{/* Jurisdiction Details Breakdown */} {jurisdictionDetails.length > 0 && (

Your Jurisdictions

When you select a city, you're connected to {jurisdictionDetails.length} levels of government:

{jurisdictionDetails.map((item: any, index: number) => { const isExpanded = expandedJurisdictions.has(index) return (
{/* Collapsed Header - Always Visible */} {/* Expanded Details */} {isExpanded && (
{/* Jurisdiction Info */}
Type
{item.type}
Name
{item.name}
{item.count !== undefined && (
Count
{item.count.toLocaleString()}
)}
{/* Discover Data Sources Button */}

Automated Data Discovery

Automatically find official websites, meeting agendas, YouTube channels, and social media for this jurisdiction.

{/* What We'll Find */}
What We'll Discover
Official Website
Meeting Agendas
YouTube Channels
Social Media
)}
) })}

💡 Why this matters: Each jurisdiction has its own meetings, budgets, and leaders that affect your daily life. Track all of them in one place.

)} {/* Two-column results layout: a left filter sidebar (lg+) that lists each content type with its real result count, and the right column with the active type's results. On small screens the sidebar is hidden and the horizontal tab bar is shown. */}
{/* Left filter sidebar — type list with counts (lg+ only). Counts come straight from tabCounts (type_totals); no fabricated numbers. */} {availableTabs.length > 0 && ( )} {/* Right column — active type's results + pagination. */}
{/* Result-type Tabs — one per type that returned hits, Decisions first. Shown below lg only; the sidebar replaces this on lg+. */} {availableTabs.length > 1 && (
)} {/* Results by Type */} {effectiveTab === 'leaders' && searchResults.results?.leaders && searchResults.results.leaders.length > 0 && (

Leaders ({searchResults.type_totals?.leaders?.toLocaleString() || searchResults.results.leaders.length})

{searchResults.results.leaders.map((result, idx) => ( ))}
)} {effectiveTab === 'persons' && searchResults.results?.persons && searchResults.results.persons.length > 0 && (

Nonprofit Leaders ({searchResults.type_totals?.persons?.toLocaleString() || searchResults.results.persons.length})

{(() => { const persons = searchResults.results.persons // Max revenue across the currently-rendered list, for bar widths. const maxRevenue = persons.reduce((max, r) => { const v = Number(r.metadata?.total_revenue) return Number.isFinite(v) && v > max ? v : max }, 0) return (
{persons.map((result, idx) => { const m = result.metadata ?? {} const revenue = Number(m.total_revenue) const hasRevenue = Number.isFinite(revenue) && m.total_revenue != null const assets = Number(m.total_assets) const hasAssets = Number.isFinite(assets) && m.total_assets != null const barWidth = hasRevenue && maxRevenue > 0 ? Math.max(2, Math.round((revenue / maxRevenue) * 100)) : 0 const cause = m.cause ?? null const dotColor = cause ? causeDotColor(String(m.ntee_code ?? m.cause)) : null return ( {/* Avatar */} {/* Leader */} {/* Organization */} {/* Cause */} {/* Total Revenue */} {/* Assets */} {/* Jurisdiction */} {/* File */} ) })}
Leader Organization Cause Total Revenue Assets Jurisdiction File
{result.title?.charAt(0) ?? '?'}
openResult(result.url)} className={`font-semibold text-gray-900 ${ result.url ? 'cursor-pointer hover:text-blue-600' : '' }`} > {result.title}
{m.title ?? 'Leader'}
{m.organization ?? '—'}
{m.ein && (
EIN {formatEin(String(m.ein))}
)}
{cause ? (
{cause}
) : null}
{hasRevenue ? (
{formatCurrency(revenue)}
) : null}
{hasAssets ? formatCurrency(assets) : null} {m.city &&
{titleCaseCity(m.city)}
} {m.state &&
{expandStateName(m.state)}
}
{m.filing_url ? ( ) : null}
) })()}
)} {effectiveTab === 'meetings' && searchResults.results?.meetings && searchResults.results.meetings.length > 0 && (

Meetings ({searchResults.type_totals?.meetings?.toLocaleString() || searchResults.results.meetings.length})

)} {effectiveTab === 'documents' && searchResults.results?.documents && searchResults.results.documents.length > 0 && (

Transcripts ({searchResults.type_totals?.documents?.toLocaleString() || searchResults.results.documents.length})

Passages from meeting transcripts that mention your search term — the discussion itself, even when it never became a titled agenda item or a recorded decision.

)} {effectiveTab === 'meeting_documents' && searchResults.results?.meeting_documents && searchResults.results.meeting_documents.length > 0 && (

Meeting Documents ({searchResults.type_totals?.meeting_documents?.toLocaleString() || searchResults.results.meeting_documents.length})

Official meeting PDFs — agendas, notes (minutes), and attachments. Click any document to open it in the PDF viewer.

{searchResults.results.meeting_documents.map((result, idx) => { const md = (result.metadata ?? {}) as Record // PDF link: prefer the explicit metadata.document_url, fall // back to result.url (both are the same direct PDF link). const pdfUrl = (md.document_url as string) || result.url || '' // Type badge label from metadata.document_type. const dt = String(md.document_type ?? '').toLowerCase() const badgeLabel = dt === 'minutes' ? 'Notes' : dt === 'attachment' ? 'Attachment' : dt === 'agenda' ? 'Agenda' : (md.document_type as string) || 'Document' const badgeColor = dt === 'minutes' ? 'bg-amber-100 text-amber-700 border-amber-200' : dt === 'attachment' ? 'bg-gray-100 text-gray-700 border-gray-200' : 'bg-cyan-100 text-cyan-700 border-cyan-200' return (
{badgeLabel} {md.body_name && ( {md.body_name} )}
{pdfUrl ? ( docViewer ? ( ) : ( {result.title} ) ) : (

{result.title}

)} {result.subtitle && (

{result.subtitle}

)}
{md.date && {md.date}} {md.jurisdiction && {md.jurisdiction}}
{/* ts_headline snippet: matched passage wrapped in literal markers. Reuse the shared highlightSnippet helper so segments are React- escaped (no dangerouslySetInnerHTML). */} {result.description && (

{highlightSnippet(result.description)}

)}
) })}
)} {effectiveTab === 'organizations' && searchResults.results?.organizations && searchResults.results.organizations.length > 0 && (

Organizations ({searchResults.type_totals?.organizations?.toLocaleString() || searchResults.results.organizations.length})

{searchResults.results.organizations.map((result, idx) => ( ))}
)} {effectiveTab === 'causes' && searchResults.results?.causes && searchResults.results.causes.length > 0 && (

Causes ({searchResults.type_totals?.causes?.toLocaleString() || searchResults.results.causes.length})

{searchResults.results.causes.map((result, idx) => ( ))}
)} {effectiveTab === 'bills' && searchResults.results?.bills && searchResults.results.bills.length > 0 && (

Bills ({searchResults.type_totals?.bills?.toLocaleString() || searchResults.results.bills.length})

{searchResults.results.bills.map((result, idx) => ( ))}
)} {effectiveTab === 'topics' && searchResults.results?.topics && searchResults.results.topics.length > 0 && (

Topics ({searchResults.type_totals?.topics?.toLocaleString() || searchResults.results.topics.length})

{searchResults.results.topics.map((result, idx) => ( ))}
)} {/* Questions — cross-jurisdiction policy questions. Generic ResultCard handles the title/subtitle/description; result.url routes to /policy-question/{id} via openResult(). */} {effectiveTab === 'questions' && searchResults.results?.questions && searchResults.results.questions.length > 0 && (

Questions ({searchResults.type_totals?.questions?.toLocaleString() || searchResults.results.questions.length})

{searchResults.results.questions.map((result, idx) => ( ))}
)} {effectiveTab === 'decisions' && searchResults.results?.decisions && searchResults.results.decisions.length > 0 && (

Decisions ({searchResults.type_totals?.decisions?.toLocaleString() || searchResults.results.decisions.length})

)} {effectiveTab === 'jurisdictions' && searchResults.results?.jurisdictions && searchResults.results.jurisdictions.length > 0 && (

Jurisdictions ({searchResults.type_totals?.jurisdictions?.toLocaleString() || searchResults.results.jurisdictions.length})

{searchResults.results.jurisdictions.map((result, idx) => ( ))}
)} {effectiveTab === 'grants' && searchResults.results?.grants && searchResults.results.grants.length > 0 && (

Grants ({searchResults.type_totals?.grants?.toLocaleString() || searchResults.results.grants.length})

{searchResults.results.grants.map((result, idx) => ( ))}
)} {/* Grant Opportunities — open federal funding (Grants.gov). Distinct from historical 990 grantmaking ("Grants") above. */} {effectiveTab === 'grant_opportunities' && searchResults.results?.grant_opportunities && searchResults.results.grant_opportunities.length > 0 && (

Grant Opportunities ({searchResults.type_totals?.grant_opportunities?.toLocaleString() || searchResults.results.grant_opportunities.length})

{searchResults.results.grant_opportunities.map((result, idx) => ( ))}
)} {/* No Results */} {searchResults.total_results === 0 && (

No results found

Try different keywords or adjust your filters

)} {/* Pagination Controls */} {searchResults.total_results > 0 && searchResults.pagination.total_pages > 1 && (
Page {searchResults.pagination.page} of {searchResults.pagination.total_pages} {searchResults.total_results} total results
{/* Page numbers */}
{Array.from({ length: Math.min(5, searchResults.pagination.total_pages) }, (_, i) => { const pageNum = Math.max( 1, Math.min( searchResults.pagination.page - 2 + i, searchResults.pagination.total_pages - 4 ) ) + Math.min(i, 4) if (pageNum > searchResults.pagination.total_pages) return null return ( ) })}
)}
{/* /right column */}
{/* /two-column results layout */} )}
)} {/* Initial State - Search Examples */} {!activeQuery && (
{(selectedState || selectedTypes.length < 5) && (

{selectedState && `State filter: ${selectedState}`} {selectedState && selectedTypes.length < 5 && ' • '} {selectedTypes.length < 5 && `Type filter: ${selectedTypes.join(', ')}`}

Enter a search query above to see results with these filters applied.

)}

Try searching for:

{[ { query: 'dental health', icon: HeartIcon, description: 'Find organizations and meetings about dental health' }, { query: 'affordable housing', icon: BuildingOfficeIcon, description: 'Discover housing-related initiatives' }, { query: 'school board', icon: CalendarIcon, description: 'View school board meetings and decisions' }, { query: 'mental health', icon: HeartIcon, description: 'Explore mental health programs and services' }, ].map((example, idx) => ( ))}
)} ) } export default function UnifiedSearch() { return ( ) }