PageSourceSearch

https://3iatlas.com/js/components/search-enhanced.js?v=1.36

js 3iatlas.com collected 2026-10-08 05:29:18 UTC 58,287 bytes, 1,465 lines download raw bytes

1/**
2 * Enhanced Global Search - v1.27.0
3 * Comprehensive search with external APIs and semantic understanding
4 * Features:
5 * - Enhanced metadata extraction (dates, sources, authors, keywords)
6 * - Fuzzy matching for typo tolerance
7 * - Advanced filtering (type, date, source credibility)
8 * - Better relevance scoring with TF-IDF
9 * - Search suggestions and auto-complete
10 * - Export results (JSON/CSV)
11 * - Improved UI with snippets and highlights
12 * - External API integration (arXiv, CrossRef)
13 * - Semantic search with natural language understanding
14 * - Question answering system
15 */
16
17class EnhancedGlobalSearch {
18    constructor() {
19        this.searchIndex = [];
20        this.searchHistory = []; // Search history disabled
21        this.currentResultIndex = -1;
22        this.results = [];
23        this.externalResults = [];
24        this.directAnswer = null;
25
26        // Get translation function
27        this.t = window.t || ((key) => key);
28
29        // Initialize external API integration
30        this.apiIntegration = window.SearchAPIIntegration ? new window.SearchAPIIntegration() : null;
31
32        // Initialize semantic search
33        this.semanticSearch = null; // Will be initialized after index is built
34        this.filters = {
35            type: 'all', // all, article, gallery, publication, faq
36            dateRange: 'all', // all, recent (last 6 months), older
37            source: 'all' // all, official (NASA/ESA), scientific, news
38        };
39        this.sortBy = 'relevance'; // relevance, date, popularity
40
41        // Keywords and synonyms for better matching
42        this.synonyms = {
43            'interstellar': ['extrasolar', 'extragalactic', 'alien'],
44            'comet': ['cometary', 'coma', 'nucleus'],
45            'perihelion': ['closest approach', 'closest to sun'],
46            'discovery': ['detected', 'found', 'identified'],
47            'orbit': ['trajectory', 'path'],
48            'observation': ['imaging', 'photograph', 'picture'],
49            'mars': ['martian'],
50            'earth': ['terrestrial'],
51            'jupiter': ['jovian']
52        };
53
54        // Source credibility scores
55        this.sourceCredibility = {
56            'nasa': 100,
57            'esa': 100,
58            'jpl': 100,
59            'nature': 90,
60            'science': 90,
61            'arxiv': 80,
62            'phys.org': 70,
63            'sciencedaily': 70,
64            'space.com': 60,
65            'iflscience': 60
66        };
67
68        this.buildSearchIndex();
69        this.initUI();
70    }
71
72    /**
73     * Build enhanced search index with metadata
74     */
75    buildSearchIndex() {
76        // console.log('[Enhanced Search] Building search index with metadata...');
77
78        // Index articles with enhanced metadata
79        document.querySelectorAll('.article-card').forEach((article, index) => {
80            const titleElem = article.querySelector('h3, [data-i18n*="article"]');
81            const sourceElem = article.querySelector('.source');
82            const linkElem = article.querySelector('a');
83
84            if (titleElem && linkElem) {
85                const title = titleElem.textContent.replace('NEW', '').trim();
86                const sourceText = sourceElem ? sourceElem.textContent : '';
87
88                // Extract date and source from "Source | Date" format
89                const parts = sourceText.split('|').map(p => p.trim());
90                const source = parts[0] || '';
91                const dateStr = parts[1] || '';
92
93                // Parse date
94                let date = null;
95                if (dateStr) {
96                    date = this.parseDate(dateStr);
97                }
98
99                // Determine if it's a research paper (arXiv) or news article
100                const isArxiv = linkElem.href.includes('arxiv.org') || source.toLowerCase().includes('arxiv');
101                const isResearchPaper = isArxiv || linkElem.href.includes('doi.org');
102                const itemType = isResearchPaper ? 'publication' : 'article';
103
104                // Calculate source credibility
105                const credibility = this.getSourceCredibility(source);
106
107                // Extract keywords from title
108                const keywords = this.extractKeywords(title);
109
110                // Check if article is marked as NEW
111                const isNew = article.querySelector('.new-badge, [data-i18n="articles.new_badge"]') !== null;
112
113                this.searchIndex.push({
114                    type: itemType,
115                    title: title,
116                    content: sourceText,
117                    url: linkElem.href,
118                    source: source,
119                    date: date,
120                    dateStr: dateStr,
121                    credibility: credibility,
122                    keywords: keywords,
123                    isNew: isNew,
124                    isArxiv: isArxiv,
125                    isPeerReviewed: !isArxiv && isResearchPaper,
126                    element: article,
127                    index: index
128                });
129            }
130        });
131
132        // Index gallery items with enhanced metadata
133        document.querySelectorAll('.gallery-item').forEach((item, index) => {
134            const titleElem = item.querySelector('h4');
135            const descElem = item.querySelector('p');
136            const metaElem = item.querySelector('.gallery-meta');
137            const linkElem = item.querySelector('a');
138            const imgElem = item.querySelector('img');
139
140            if (titleElem && linkElem) {
141                const title = titleElem.textContent.replace('NEW', '').trim();
142                const description = descElem ? descElem.textContent.trim() : '';
143                const metaText = metaElem ? metaElem.textContent : '';
144
145                // Extract photographer/source and date from metadata
146                const parts = metaText.split('•').map(p => p.trim());
147                const photographer = parts[0] || '';
148                const dateStr = parts[1] || '';
149
150                let date = null;
151                if (dateStr) {
152                    date = this.parseDate(dateStr);
153                }
154
155                // Extract alt text and keywords
156                const altText = imgElem ? imgElem.alt : '';
157                const keywords = this.extractKeywords(title + ' ' + description + ' ' + altText);
158
159                const isNew = item.querySelector('.new-badge, [data-i18n="articles.new_badge"]') !== null;
160
161                this.searchIndex.push({
162                    type: 'gallery',
163                    title: title,
164                    content: description,
165                    url: linkElem.href,
166                    source: photographer,
167                    date: date,
168                    dateStr: dateStr,
169                    credibility: 70, // Default for gallery
170                    keywords: keywords,
171                    altText: altText,
172                    isNew: isNew,
173                    element: item,
174                    index: index
175                });
176            }
177        });
178
179        // Index publications with enhanced metadata
180        document.querySelectorAll('.publication-item, .article-link').forEach((pub, index) => {
181            const titleElem = pub.querySelector('strong, b') || pub;
182            const linkElem = pub.querySelector('a') || pub;
183
184            if (titleElem && linkElem.href) {
185                const title = titleElem.textContent.trim();
186
187                // Detect if it's arXiv or peer-reviewed
188                const isArxiv = linkElem.href.includes('arxiv.org');
189                const isPeerReviewed = !isArxiv && (
190                    linkElem.href.includes('nature.com') ||
191                    linkElem.href.includes('science.org') ||
192                    linkElem.href.includes('doi.org')
193                );
194
195                const credibility = isPeerReviewed ? 90 : (isArxiv ? 80 : 70);
196                const keywords = this.extractKeywords(title);
197
198                this.searchIndex.push({
199                    type: 'publication',
200                    title: title,
201                    content: '',
202                    url: linkElem.href,
203                    source: isArxiv ? 'arXiv' : (isPeerReviewed ? 'Peer-Reviewed' : 'Publication'),
204                    date: null,
205                    dateStr: '',
206                    credibility: credibility,
207                    keywords: keywords,
208                    isPeerReviewed: isPeerReviewed,
209                    isArxiv: isArxiv,
210                    element: pub,
211                    index: index
212                });
213            }
214        });
215
216        // Index FAQ items
217        document.querySelectorAll('.faq-item').forEach((faq, index) => {
218            const questionElem = faq.querySelector('.faq-question');
219            const answerElem = faq.querySelector('.faq-answer');
220
221            if (questionElem) {
222                const question = questionElem.textContent.trim();
223                const answer = answerElem ? answerElem.textContent.trim() : '';
224                const keywords = this.extractKeywords(question + ' ' + answer);
225
226                this.searchIndex.push({
227                    type: 'faq',
228                    title: question,
229                    content: answer,
230                    url: '#' + (faq.id || 'faq'),
231                    source: 'FAQ',
232                    date: null,
233                    dateStr: '',
234                    credibility: 100, // Official FAQ
235                    keywords: keywords,
236                    element: faq,
237                    index: index
238                });
239            }
240        });
241
242        // Build term frequency map for TF-IDF
243        this.buildTermFrequencyMap();
244
245        // Initialize semantic search now that index is built
246        if (window.SemanticSearch) {
247            this.semanticSearch = new window.SemanticSearch(this.searchIndex);
248            // console.log('[Enhanced Search] Semantic search initialized');
249        }
250
251        // console.log(`[Enhanced Search] Index built: ${this.searchIndex.length} items`);
252        // console.log(`[Enhanced Search] Types: Articles=${this.searchIndex.filter(i => i.type === 'article').length}, Gallery=${this.searchIndex.filter(i => i.type === 'gallery').length}, Publications=${this.searchIndex.filter(i => i.type === 'publication').length}, FAQs=${this.searchIndex.filter(i => i.type === 'faq').length}`);
253
254        // Build autocomplete suggestions
255        this.buildSuggestions();
256
257        // Optionally pre-load external API results for 3I/ATLAS
258        if (this.apiIntegration) {
259            // console.log('[Enhanced Search] External APIs available');
260        }
261    }
262
263    /**
264     * Build autocomplete suggestions from search index
265     */
266    buildSuggestions() {
267        this.suggestions = new Set();
268
269        // Common search queries - comprehensive list
270        const commonQueries = [
271            // Mission milestones
272            'perihelion', 'discovery', 'closest approach', 'when discovered', 'when perihelion',
273
274            // Physical properties
275            'composition', 'water', 'size', 'diameter', 'mass', 'coma', 'tail',
276            'carbon dioxide', 'co2', 'cyanide', 'nickel',
277
278            // Orbital mechanics
279            'orbit', 'trajectory', 'speed', 'velocity', 'distance', 'eccentricity',
280            'hyperbolic', 'orbital period',
281
282            // Celestial bodies
283            'mars', 'earth', 'jupiter', 'sun', 'solar system',
284
285            // Observations
286            'telescope', 'images', 'pictures', 'photos', 'observations',
287            'hubble', 'webb', 'swift', 'atlas',
288
289            // Scientific content
290            'papers', 'research', 'articles', 'publications', 'arxiv',
291
292            // Organizations
293            'nasa', 'esa', 'jpl', 'cnsa',
294
295            // General
296            'comet', 'interstellar', 'alien', 'extrasolar',
297
298            // Questions
299            'when', 'what', 'how', 'where', 'why', 'who'
300        ];
301
302        commonQueries.forEach(q => this.suggestions.add(q.toLowerCase()));
303
304        // Extract popular keywords from titles
305        this.searchIndex.forEach(item => {
306            const titleWords = item.title.toLowerCase()
307                .replace(/[^\w\s]/g, ' ')
308                .split(/\s+/)
309                .filter(word => word.length > 3);
310
311            titleWords.slice(0, 3).forEach(word => this.suggestions.add(word)); // Top 3 words from each title
312        });
313
314        this.suggestionsArray = Array.from(this.suggestions).sort();
315        // console.log(`[Enhanced Search] Built ${this.suggestionsArray.length} autocomplete suggestions`);
316    }
317
318    /**
319     * Get autocomplete suggestions for a query
320     */
321    getAutocomplete(query) {
322        if (!query || query.length < 2) return [];
323
324        const lowerQuery = query.toLowerCase();
325
326        return this.suggestionsArray
327            .filter(suggestion => suggestion.includes(lowerQuery))
328            .sort((a, b) => {
329                // Prioritize suggestions that start with the query
330                const aStarts = a.startsWith(lowerQuery);
331                const bStarts = b.startsWith(lowerQuery);
332                if (aStarts && !bStarts) return -1;
333                if (!aStarts && bStarts) return 1;
334                return a.length - b.length; // Shorter suggestions first
335            })
336            .slice(0, 5); // Top 5 suggestions
337    }
338
339    /**
340     * Extract keywords from text
341     */
342    extractKeywords(text) {
343        // Remove common stop words
344        const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those', 'it', 'its', 'they', 'their', 'them']);
345
346        const words = text.toLowerCase()
347            .replace(/[^\w\s]/g, ' ')
348            .split(/\s+/)
349            .filter(word => word.length > 2 && !stopWords.has(word));
350
351        return [...new Set(words)]; // Unique keywords
352    }
353
354    /**
355     * Parse date from various formats
356     */
357    parseDate(dateStr) {
358        // Try to parse "Month DD, YYYY" format
359        const monthNames = {
360            'january': 0, 'jan': 0,
361            'february': 1, 'feb': 1,
362            'march': 2, 'mar': 2,
363            'april': 3, 'apr': 3,
364            'may': 4,
365            'june': 5, 'jun': 5,
366            'july': 6, 'jul': 6,
367            'august': 7, 'aug': 7,
368            'september': 8, 'sep': 8, 'sept': 8,
369            'october': 9, 'oct': 9,
370            'november': 10, 'nov': 10,
371            'december': 11, 'dec': 11
372        };
373
374        // Prefer the release date in parentheses, e.g. "(Released July 6, 2026)"
375        const releasedMatch = dateStr.match(/\((?:Released\s+)?([a-z]+\s+\d{1,2},?\s+\d{4})\)/i);
376        const candidateStr = releasedMatch ? releasedMatch[1] : dateStr;
377
378        // Match "Month DD, YYYY"
379        const match = candidateStr.match(/([a-z]+)\s+(\d{1,2}),?\s+(\d{4})/i);
380        if (match) {
381            const month = monthNames[match[1].toLowerCase()];
382            const day = parseInt(match[2]);
383            const year = parseInt(match[3]);
384
385            if (month !== undefined && day && year) {
386                return new Date(year, month, day);
387            }
388        }
389
390        // Fallback: try Date.parse
391        const parsed = Date.parse(dateStr);
392        return isNaN(parsed) ? null : new Date(parsed);
393    }
394
395    /**
396     * Get source credibility score
397     */
398    getSourceCredibility(source) {
399        const sourceLower = source.toLowerCase();
400
401        for (const [key, score] of Object.entries(this.sourceCredibility)) {
402            if (sourceLower.includes(key)) {
403                return score;
404            }
405        }
406
407        return 50; // Default credibility
408    }
409
410    /**
411     * Build term frequency map for TF-IDF
412     */
413    buildTermFrequencyMap() {
414        this.termDocumentFrequency = {};
415
416        this.searchIndex.forEach(item => {
417            const terms = new Set([...item.keywords, ...item.title.toLowerCase().split(/\s+/)]);
418
419            terms.forEach(term => {
420                if (!this.termDocumentFrequency[term]) {
421                    this.termDocumentFrequency[term] = 0;
422                }
423                this.termDocumentFrequency[term]++;
424            });
425        });
426    }
427
428    /**
429     * Calculate TF-IDF score
430     */
431    calculateTFIDF(item, term) {
432        const termFreq = item.keywords.filter(k => k === term).length;
433        const docFreq = this.termDocumentFrequency[term] || 1;
434        const idf = Math.log(this.searchIndex.length / docFreq);
435
436        return termFreq * idf;
437    }
438
439    /**
440     * Levenshtein distance for fuzzy matching
441     */
442    levenshteinDistance(str1, str2) {
443        const len1 = str1.length;
444        const len2 = str2.length;
445        const matrix = [];
446
447        for (let i = 0; i <= len1; i++) {
448            matrix[i] = [i];
449        }
450
451        for (let j = 0; j <= len2; j++) {
452            matrix[0][j] = j;
453        }
454
455        for (let i = 1; i <= len1; i++) {
456            for (let j = 1; j <= len2; j++) {
457                if (str1[i - 1] === str2[j - 1]) {
458                    matrix[i][j] = matrix[i - 1][j - 1];
459                } else {
460                    matrix[i][j] = Math.min(
461                        matrix[i - 1][j - 1] + 1, // substitution
462                        matrix[i][j - 1] + 1,     // insertion
463                        matrix[i - 1][j] + 1      // deletion
464                    );
465                }
466            }
467        }
468
469        return matrix[len1][len2];
470    }
471
472    /**
473     * Fuzzy match with typo tolerance
474     */
475    fuzzyMatch(query, target, threshold = 2) {
476        const distance = this.levenshteinDistance(query.toLowerCase(), target.toLowerCase());
477        const maxLen = Math.max(query.length, target.length);
478
479        // Allow up to threshold edits OR 20% difference for longer words
480        return distance <= threshold || distance <= maxLen * 0.2;
481    }
482
483    /**
484     * Enhanced search with fuzzy matching, filters, and TF-IDF scoring
485     */
486    search(query) {
487        if (!query || query.length < 2) return [];
488
489        query = query.toLowerCase().trim();
490        let words = query.split(/\s+/);
491
492        // Semantic search processing
493        let semanticResults = null;
494        if (this.semanticSearch) {
495            semanticResults = this.semanticSearch.search(query, []);
496
497            // Store direct answer if found
498            this.directAnswer = semanticResults.answer;
499
500            // Expand query with semantic terms
501            if (semanticResults.expandedTerms && semanticResults.expandedTerms.length > 0) {
502                words = [...words, ...semanticResults.expandedTerms];
503            }
504        }
505
506        // Apply filters
507        let filteredIndex = [...this.searchIndex];
508
509        // Type filter
510        if (this.filters.type !== 'all') {
511            filteredIndex = filteredIndex.filter(item => item.type === this.filters.type);
512        }
513
514        // Date range filter
515        if (this.filters.dateRange !== 'all') {
516            const now = new Date();
517            const sixMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
518
519            if (this.filters.dateRange === 'recent') {
520                filteredIndex = filteredIndex.filter(item => item.date && item.date >= sixMonthsAgo);
521            } else if (this.filters.dateRange === 'older') {
522                filteredIndex = filteredIndex.filter(item => item.date && item.date < sixMonthsAgo);
523            }
524        }
525
526        // Source credibility filter
527        if (this.filters.source === 'official') {
528            filteredIndex = filteredIndex.filter(item => item.credibility >= 90);
529        } else if (this.filters.source === 'scientific') {
530            filteredIndex = filteredIndex.filter(item => item.credibility >= 70);
531        }
532
533        // Search with enhanced scoring (includes semantic boost)
534        const localResults = filteredIndex
535            .map(item => {
536                const baseScore = this.calculateRelevance(item, query, words);
537                const semanticBoost = semanticResults && this.semanticSearch ?
538                    this.semanticSearch.scoreSemanticRelevance(item, query, semanticResults.intent) : 0;
539
540                return {
541                    ...item,
542                    score: baseScore + semanticBoost
543                };
544            })
545            .filter(item => item.score > 0)
546            .sort((a, b) => {
547                // Apply sorting based on user preference
548                if (this.sortBy === 'date') {
549                    // Sort by date (newest first)
550                    if (!a.date && !b.date) return b.score - a.score; // Fallback to relevance
551                    if (!a.date) return 1; // Items without date go to end
552                    if (!b.date) return -1;
553                    return b.date - a.date;
554                } else if (this.sortBy === 'popularity') {
555                    // Sort by credibility score (proxy for popularity)
556                    const credDiff = b.credibility - a.credibility;
557                    if (credDiff !== 0) return credDiff;
558                    return b.score - a.score; // Fallback to relevance
559                } else {
560                    // Default: Sort by relevance score
561                    return b.score - a.score;
562                }
563            })
564            .slice(0, 20); // Top 20 local results
565
566        return localResults;
567    }
568
569    /**
570     * Calculate relevance score with TF-IDF and fuzzy matching
571     */
572    calculateRelevance(item, query, words) {
573        let score = 0;
574        const title = item.title.toLowerCase();
575        const content = item.content.toLowerCase();
576        const keywords = item.keywords.join(' ');
577
578        // Exact title match = highest score
579        if (title === query) {
580            score += 200;
581        }
582        // Title starts with query
583        else if (title.startsWith(query)) {
584            score += 150;
585        }
586        // Title contains query
587        else if (title.includes(query)) {
588            score += 100;
589        }
590
591        // Check each word with fuzzy matching
592        words.forEach(word => {
593            // Exact keyword match
594            if (item.keywords.includes(word)) {
595                score += 30 + this.calculateTFIDF(item, word) * 10;
596            }
597
598            // Fuzzy keyword match
599            else {
600                const fuzzyMatches = item.keywords.filter(k => this.fuzzyMatch(word, k));
601                if (fuzzyMatches.length > 0) {
602                    score += 20;
603                }
604            }
605
606            // Title word match
607            if (title.includes(word)) {
608                score += 25;
609            }
610
611            // Content word match
612            if (content.includes(word)) {
613                score += 10;
614            }
615
616            // Synonym matching
617            for (const [key, synonyms] of Object.entries(this.synonyms)) {
618                if (word === key || synonyms.includes(word)) {
619                    const allTerms = [key, ...synonyms];
620                    const matchesKeywords = allTerms.some(term => item.keywords.includes(term));
621                    const matchesTitle = allTerms.some(term => title.includes(term));
622
623                    if (matchesKeywords) score += 15;
624                    if (matchesTitle) score += 10;
625                }
626            }
627        });
628
629        // Boost score based on metadata
630
631        // Credibility boost
632        score += item.credibility * 0.3;
633
634        // Recency boost (newer content gets slight boost)
635        if (item.date) {
636            const daysSincePublish = (new Date() - item.date) / (1000 * 60 * 60 * 24);
637            if (daysSincePublish < 180) { // Last 6 months
638                score += 20;
639            }
640        }
641
642        // NEW badge boost
643        if (item.isNew) {
644            score += 15;
645        }
646
647        // Peer-reviewed boost
648        if (item.isPeerReviewed) {
649            score += 25;
650        }
651
652        return score;
653    }
654
655    /**
656     * Get search suggestions based on query
657     */
658    getSuggestions(query) {
659        if (!query || query.length < 2) return [];
660
661        const suggestions = new Set();
662        const queryLower = query.toLowerCase();
663
664        // Search in titles
665        this.searchIndex.forEach(item => {
666            const titleWords = item.title.toLowerCase().split(/\s+/);
667            titleWords.forEach(word => {
668                if (word.startsWith(queryLower) && word.length > 2) {
669                    suggestions.add(word);
670                }
671            });
672        });
673
674        // Add synonyms
675        Object.keys(this.synonyms).forEach(key => {
676            if (key.startsWith(queryLower)) {
677                suggestions.add(key);
678            }
679        });
680
681        return Array.from(suggestions).slice(0, 5);
682    }
683
684    /**
685     * Export search results
686     */
687    exportResults(format = 'json') {
688        if (this.results.length === 0) {
689            alert('No results to export');
690            return;
691        }
692
693        const exportData = this.results.map(item => ({
694            title: item.title,
695            type: item.type,
696            url: item.url,
697            source: item.source,
698            date: item.dateStr,
699            credibility: item.credibility,
700            relevance_score: item.score.toFixed(2)
701        }));
702
703        let content, filename, mimeType;
704
705        if (format === 'json') {
706            content = JSON.stringify(exportData, null, 2);
707            filename = `3iatlas-search-results-${Date.now()}.json`;
708            mimeType = 'application/json';
709        } else if (format === 'csv') {
710            const headers = ['Title', 'Type', 'URL', 'Source', 'Date', 'Credibility', 'Relevance Score'];
711            const rows = exportData.map(item => [
712                `"${item.title.replace(/"/g, '""')}"`,
713                item.type,
714                item.url,
715                `"${item.source.replace(/"/g, '""')}"`,
716                item.date,
717                item.credibility,
718                item.relevance_score
719            ]);
720
721            content = headers.join(',') + '\n' + rows.map(r => r.join(',')).join('\n');
722            filename = `3iatlas-search-results-${Date.now()}.csv`;
723            mimeType = 'text/csv';
724        }
725
726        // Download file
727        const blob = new Blob([content], { type: mimeType });
728        const link = document.createElement('a');
729        link.href = URL.createObjectURL(blob);
730        link.download = filename;
731        link.click();
732        URL.revokeObjectURL(link.href);
733
734        if (window.Analytics) {
735            Analytics.trackEvent('search-export', { format, count: this.results.length });
736        }
737    }
738
739    /**
740     * Initialize enhanced search UI
741     */
742    initUI() {
743        // Check if inline container exists (for inline search below Mission Control)
744        const inlineContainer = document.getElementById('inline-search-container');
745
746        if (inlineContainer) {
747            // Create inline search UI
748            inlineContainer.innerHTML = `
749                <div class="search-container inline" role="search" aria-label="Search">
750                    <div class="search-input-wrapper">
751                        <i class="fas fa-search" aria-hidden="true"></i>
752                        <input
753                            type="text"
754                            id="global-search-input"
755                            placeholder="${this.t('search.placeholder')}"
756                            autocomplete="off"
757                            aria-label="Search input"
758                            list="search-autocomplete"
759                        />
760                        <datalist id="search-autocomplete"></datalist>
761                        <button class="search-clear" aria-label="Clear search" style="display: none;">
762                            <i class="fas fa-times-circle"></i>
763                        </button>
764                    </div>
765
766                    <!-- Search Filters -->
767                    <div class="search-filters">
768                        <div class="filter-group">
769                            <label class="filter-label">${this.t('search.filter.type')}</label>
770                            <button class="filter-btn active" data-filter-type="all">${this.t('search.filter.type.all')}</button>
771                            <button class="filter-btn" data-filter-type="article">${this.t('search.filter.type.articles')}</button>
772                            <button class="filter-btn" data-filter-type="gallery">${this.t('search.filter.type.gallery')}</button>
773                            <button class="filter-btn" data-filter-type="publication">${this.t('search.filter.type.papers')}</button>
774                            <button class="filter-btn" data-filter-type="faq">${this.t('search.filter.type.faqs')}</button>
775                        </div>
776                        <div class="filter-group">
777                            <label class="filter-label">${this.t('search.filter.date')}</label>
778                            <button class="filter-btn active" data-filter-date="all">${this.t('search.filter.date.all')}</button>
779                            <button class="filter-btn" data-filter-date="recent">${this.t('search.filter.date.recent')}</button>
780                            <button class="filter-btn" data-filter-date="older">${this.t('search.filter.date.older')}</button>
781                        </div>
782                        <div class="filter-group">
783                            <label class="filter-label">${this.t('search.filter.source')}</label>
784                            <button class="filter-btn active" data-filter-source="all">${this.t('search.filter.source.all')}</button>
785                            <button class="filter-btn" data-filter-source="official">${this.t('search.filter.source.official')}</button>
786                            <button class="filter-btn" data-filter-source="scientific">${this.t('search.filter.source.scientific')}</button>
787                        </div>
788                        <div class="filter-group">
789                            <label class="filter-label">${this.t('search.sort')}</label>
790                            <button class="sort-btn active" data-sort="relevance">${this.t('search.sort.relevance')}</button>
791                            <button class="sort-btn" data-sort="date">${this.t('search.sort.date')}</button>
792                            <button class="sort-btn" data-sort="popularity">${this.t('search.sort.popularity')}</button>
793                        </div>
794                    </div>
795
796                    <!-- Result count -->
797                    <div id="search-result-count" class="search-result-count" style="display: none;"></div>
798
799                    <!-- Results container -->
800                    <div id="search-results" class="search-results" role="listbox" aria-label="Search results"></div>
801                </div>
802            `;
803        } else {
804            // Create search overlay with enhanced UI (modal version)
805            const searchOverlay = document.createElement('div');
806            searchOverlay.id = 'search-overlay';
807            searchOverlay.className = 'search-overlay';
808            searchOverlay.innerHTML = `
809                <div class="search-container" role="dialog" aria-label="Search" aria-modal="true">
810                    <div class="search-input-wrapper">
811                        <i class="fas fa-search" aria-hidden="true"></i>
812                        <input
813                            type="text"
814                            id="global-search-input"
815                            placeholder="${this.t('search.placeholder')}"
816                            autocomplete="off"
817                            aria-label="Search input"
818                            list="search-autocomplete"
819                        />
820                        <datalist id="search-autocomplete"></datalist>
821                        <button class="search-clear" aria-label="Clear search" style="display: none;">
822                            <i class="fas fa-times-circle"></i>
823                        </button>
824                        <button class="search-close" aria-label="Close search">
825                            <i class="fas fa-times"></i>
826                        </button>
827                    </div>
828
829                    <!-- Search Filters -->
830                    <div class="search-filters">
831                        <div class="filter-group">
832                            <label class="filter-label">${this.t('search.filter.type')}</label>
833                            <button class="filter-btn active" data-filter-type="all">${this.t('search.filter.type.all')}</button>
834                            <button class="filter-btn" data-filter-type="article">${this.t('search.filter.type.articles')}</button>
835                            <button class="filter-btn" data-filter-type="gallery">${this.t('search.filter.type.gallery')}</button>
836                            <button class="filter-btn" data-filter-type="publication">${this.t('search.filter.type.papers')}</button>
837                            <button class="filter-btn" data-filter-type="faq">${this.t('search.filter.type.faqs')}</button>
838                        </div>
839                        <div class="filter-group">
840                            <label class="filter-label">${this.t('search.filter.date')}</label>
841                            <button class="filter-btn active" data-filter-date="all">${this.t('search.filter.date.all')}</button>
842                            <button class="filter-btn" data-filter-date="recent">${this.t('search.filter.date.recent')}</button>
843                            <button class="filter-btn" data-filter-date="older">${this.t('search.filter.date.older')}</button>
844                        </div>
845                        <div class="filter-group">
846                            <label class="filter-label">${this.t('search.filter.source')}</label>
847                            <button class="filter-btn active" data-filter-source="all">${this.t('search.filter.source.all')}</button>
848                            <button class="filter-btn" data-filter-source="official">${this.t('search.filter.source.official')}</button>
849                            <button class="filter-btn" data-filter-source="scientific">${this.t('search.filter.source.scientific')}</button>
850                        </div>
851                        <div class="filter-group">
852                            <label class="filter-label">${this.t('search.sort')}</label>
853                            <button class="sort-btn active" data-sort="relevance">${this.t('search.sort.relevance')}</button>
854                            <button class="sort-btn" data-sort="date">${this.t('search.sort.date')}</button>
855                            <button class="sort-btn" data-sort="popularity">${this.t('search.sort.popularity')}</button>
856                        </div>
857                    </div>
858
859                    <!-- Result count -->
860                    <div id="search-result-count" class="search-result-count" style="display: none;"></div>
861
862                    <!-- Results container -->
863                    <div id="search-results" class="search-results" role="listbox" aria-label="Search results"></div>
864
865                    <!-- Footer -->
866                    <div class="search-footer">
867                        <span class="search-hint">
868                            <kbd>↑</kbd> <kbd>↓</kbd> navigate · <kbd>Enter</kbd> select · <kbd>Esc</kbd> close
869                        </span>
870                    </div>
871                </div>
872            `;
873            document.body.appendChild(searchOverlay);
874        }
875
876        // Get elements
877        const input = document.getElementById('global-search-input');
878        const container = inlineContainer || document.getElementById('search-overlay');
879        const closeBtn = container.querySelector('.search-close');
880        const clearBtn = container.querySelector('.search-clear');
881        const resultsContainer = document.getElementById('search-results');
882        const resultCount = document.getElementById('search-result-count');
883
884        // Input event with debouncing
885        let searchTimeout;
886        input.addEventListener('input', (e) => {
887            const query = e.target.value;
888            clearBtn.style.display = query ? 'block' : 'none';
889
890            // Update autocomplete suggestions
891            const datalist = document.getElementById('search-autocomplete');
892            if (datalist && query.length >= 2) {
893                const suggestions = this.getAutocomplete(query);
894                datalist.innerHTML = suggestions.map(s => `<option value="${s}">`).join('');
895            } else if (datalist) {
896                datalist.innerHTML = '';
897            }
898
899            clearTimeout(searchTimeout);
900            searchTimeout = setTimeout(() => {
901                this.handleSearch(query);
902            }, 200); // Debounce 200ms
903        });
904
905        // Clear button
906        clearBtn.addEventListener('click', () => {
907            input.value = '';
908            clearBtn.style.display = 'none';
909            resultsContainer.innerHTML = '';
910            resultCount.style.display = 'none';
911            input.focus();
912        });
913
914        // Close button (only for modal)
915        if (closeBtn) {
916            closeBtn.addEventListener('click', () => {
917                this.closeSearch();
918            });
919        }
920
921        // Type filter buttons
922        const filterBtns = container.querySelectorAll('[data-filter-type]');
923        filterBtns.forEach(btn => {
924            btn.addEventListener('click', () => {
925                const filterType = btn.getAttribute('data-filter-type');
926                this.filters.type = filterType;
927
928                // Update active state within same group
929                const parent = btn.parentElement;
930                parent.querySelectorAll('[data-filter-type]').forEach(b => b.classList.remove('active'));
931                btn.classList.add('active');
932
933                // Re-run search
934                this.handleSearch(input.value);
935            });
936        });
937
938        // Date filter buttons
939        const dateBtns = container.querySelectorAll('[data-filter-date]');
940        dateBtns.forEach(btn => {
941            btn.addEventListener('click', () => {
942                const filterDate = btn.getAttribute('data-filter-date');
943                this.filters.dateRange = filterDate;
944
945                // Update active state within same group
946                const parent = btn.parentElement;
947                parent.querySelectorAll('[data-filter-date]').forEach(b => b.classList.remove('active'));
948                btn.classList.add('active');
949
950                // Re-run search
951                this.handleSearch(input.value);
952            });
953        });
954
955        // Source filter buttons
956        const sourceBtns = container.querySelectorAll('[data-filter-source]');
957        sourceBtns.forEach(btn => {
958            btn.addEventListener('click', () => {
959                const filterSource = btn.getAttribute('data-filter-source');
960                this.filters.source = filterSource;
961
962                // Update active state within same group
963                const parent = btn.parentElement;
964                parent.querySelectorAll('[data-filter-source]').forEach(b => b.classList.remove('active'));
965                btn.classList.add('active');
966
967                // Re-run search
968                this.handleSearch(input.value);
969            });
970        });
971
972        // Sort buttons
973        const sortBtns = container.querySelectorAll('.sort-btn');
974        sortBtns.forEach(btn => {
975            btn.addEventListener('click', () => {
976                const sortType = btn.getAttribute('data-sort');
977                this.sortBy = sortType;
978
979                // Update active state within same group
980                const parent = btn.parentElement;
981                parent.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
982                btn.classList.add('active');
983
984                // Re-run search
985                this.handleSearch(input.value);
986            });
987        });
988
989        // Keyboard navigation
990        input.addEventListener('keydown', (e) => {
991            if (e.key === 'ArrowDown') {
992                e.preventDefault();
993                this.navigateResults(1);
994            } else if (e.key === 'ArrowUp') {
995                e.preventDefault();
996                this.navigateResults(-1);
997            } else if (e.key === 'Enter') {
998                e.preventDefault();
999                this.selectResult();
1000            }
1001        });
1002
1003        // Click overlay to close (only for modal)
1004        if (!inlineContainer) {
1005            const searchOverlay = document.getElementById('search-overlay');
1006            searchOverlay.addEventListener('click', (e) => {
1007                if (e.target === searchOverlay) {
1008                    this.closeSearch();
1009                }
1010            });
1011        }
1012
1013        // Search toggle button in header (only for modal, hide if inline)
1014        const searchToggle = document.getElementById('searchToggle');
1015        if (inlineContainer && searchToggle) {
1016            // Hide search toggle button if search is inline
1017            searchToggle.style.display = 'none';
1018            // Show initial state for inline search
1019            this.handleSearch('');
1020        } else if (searchToggle) {
1021            searchToggle.addEventListener('click', () => {
1022                this.openSearch();
1023            });
1024        }
1025    }
1026
1027    /**
1028     * Handle search query
1029     */
1030    handleSearch(query) {
1031        this.results = this.search(query);
1032        this.currentResultIndex = -1;
1033        const resultsContainer = document.getElementById('search-results');
1034        const resultCount = document.getElementById('search-result-count');
1035
1036        if (!query || query.length < 2) {
1037            resultsContainer.innerHTML = this.renderSearchHistory();
1038            resultCount.style.display = 'none';
1039            this.directAnswer = null;
1040            this.externalResults = [];
1041            return;
1042        }
1043
1044        // Track search query in analytics
1045        if (window.Analytics) {
1046            Analytics.trackEvent('search-query', {
1047                query: query.substring(0, 50), // Limit query length for privacy
1048                resultCount: this.results.length
1049            });
1050        }
1051
1052        // Build results HTML
1053        let resultsHTML = '';
1054
1055        // Show direct answer if available
1056        if (this.directAnswer) {
1057            resultsHTML += `
1058                <div class="search-direct-answer">
1059                    <div class="direct-answer-header">
1060                        <i class="fas fa-lightbulb"></i>
1061                        <span>Direct Answer</span>
1062                        <span class="answer-confidence">${Math.round(this.directAnswer.confidence * 100)}% confident</span>
1063                    </div>
1064                    <div class="direct-answer-content">
1065                        ${this.semanticSearch ? this.semanticSearch.highlightAnswer(this.directAnswer.answer, query) : this.escapeHtml(this.directAnswer.answer)}
1066                    </div>
1067                    <div class="direct-answer-source">
1068                        <i class="fas fa-info-circle"></i> Source: ${this.escapeHtml(this.directAnswer.source)}
1069                    </div>
1070                </div>
1071            `;
1072        }
1073
1074        // Start external API search asynchronously - DISABLED for v1.25
1075        // if (this.apiIntegration && query.length >= 3) {
1076        //     this.searchExternalAPIs(query);
1077        // }
1078
1079        // Show result count
1080        const totalCount = this.results.length + this.externalResults.length;
1081        resultCount.textContent = `${totalCount} result${totalCount !== 1 ? 's' : ''} found${this.externalResults.length > 0 ? ` (${this.externalResults.length} from external sources)` : ''}`;
1082        resultCount.style.display = 'block';
1083
1084        if (this.results.length === 0 && this.externalResults.length === 0 && !this.directAnswer) {
1085            // Track zero results for analytics
1086            if (window.Analytics) {
1087                Analytics.trackEvent('search-no-results', {
1088                    query: query.substring(0, 50)
1089                });
1090            }
1091
1092            resultsContainer.innerHTML = `
1093                <div class="search-no-results">
1094                    <i class="fas fa-search" aria-hidden="true"></i>
1095                    <p>No results found for "${this.escapeHtml(query)}"</p>
1096                    <p class="search-tip">Try different keywords or check spelling</p>
1097                    ${this.apiIntegration ? '<p class="search-tip"><i class="fas fa-spinner fa-spin"></i> Searching external sources...</p>' : ''}
1098                </div>
1099            `;
1100            return;
1101        }
1102
1103        // Render local results
1104        resultsHTML += this.results.map((item, index) => `
1105            <div class="search-result-item" data-index="${index}" data-url="${this.escapeHtml(item.url)}">
1106                <div class="search-result-icon">
1107                    <i class="fas fa-${this.getIcon(item.type)}" aria-hidden="true"></i>
1108                </div>
1109                <div class="search-result-content">
1110                    <div class="search-result-title">${this.highlightMatch(item.title, query)}</div>
1111                    <div class="search-result-meta">
1112                        <span class="search-result-type">${this.getTypeLabel(item.type)}</span>
1113                        ${item.source ? `<span>·</span><span>${this.escapeHtml(item.source)}</span>` : ''}
1114                        ${item.dateStr ? `<span>·</span><span>${this.escapeHtml(item.dateStr)}</span>` : ''}
1115                        ${item.credibility >= 90 ? '<span class="badge-verified"><i class="fas fa-check-circle"></i> Verified</span>' : ''}
1116                        ${item.isPeerReviewed ? '<span class="badge-peer-reviewed">Peer-Reviewed</span>' : ''}
1117                    </div>
1118                    ${item.content ? `<div class="search-result-preview">${this.createSnippet(item.content, query)}</div>` : ''}
1119                </div>
1120                <div class="search-result-score" title="Relevance score">
1121                    ${Math.round(item.score)}
1122                </div>
1123            </div>
1124        `).join('');
1125
1126        // Set results HTML
1127        resultsContainer.innerHTML = resultsHTML;
1128
1129        // Add click handlers
1130        resultsContainer.querySelectorAll('.search-result-item').forEach((item, index) => {
1131            item.addEventListener('click', () => {
1132                const url = item.getAttribute('data-url');
1133                const result = this.results[index];
1134
1135                // Track result click in analytics
1136                if (window.Analytics && result) {
1137                    Analytics.trackEvent('search-result-click', {
1138                        query: query.substring(0, 50),
1139                        type: result.type,
1140                        position: index + 1,
1141                        title: result.title.substring(0, 50)
1142                    });
1143                }
1144
1145                this.addToHistory(query);
1146                window.open(url, '_blank');
1147            });
1148        });
1149    }
1150
1151    /**
1152     * Search external APIs asynchronously
1153     */
1154    async searchExternalAPIs(query) {
1155        if (!this.apiIntegration) return;
1156
1157        const resultsContainer = document.getElementById('search-results');
1158        const resultCount = document.getElementById('search-result-count');
1159
1160        // Show loading indicator
1161        const loadingDiv = document.createElement('div');
1162        loadingDiv.className = 'search-loading';
1163        loadingDiv.innerHTML = `
1164            <div class="loading-spinner">
1165                <i class="fas fa-spinner fa-spin"></i>
1166                <span>Searching arXiv and CrossRef...</span>
1167            </div>
1168        `;
1169        resultsContainer.appendChild(loadingDiv);
1170
1171        try {
1172            // Search all external APIs
1173            const externalResults = await this.apiIntegration.searchAll(query);
1174
1175            this.externalResults = externalResults;
1176
1177            // Remove loading indicator
1178            loadingDiv.remove();
1179
1180            // External results section disabled
1181            // if (externalResults.length > 0) {
1182            //     // Add external results to the display
1183            //     const externalHTML = `
1184            //         <div class="search-section-header">
1185            //             <i class="fas fa-cloud"></i>
1186            //             <span>External Sources (${externalResults.length})</span>
1187            //         </div>
1188            //     ` + externalResults.map((item, index) => `
1189            //         <div class="search-result-item external-result" data-index="${index}" data-url="${this.escapeHtml(item.url)}">
1190            //             <div class="search-result-icon">
1191            //                 <i class="fas fa-${this.getIcon(item.type)}" aria-hidden="true"></i>
1192            //             </div>
1193            //             <div class="search-result-content">
1194            //                 <div class="search-result-title">${this.escapeHtml(item.title)}</div>
1195            //                 <div class="search-result-meta">
1196            //                     <span class="search-result-type">${item.type}</span>
1197            //                     ${item.source ? `<span>·</span><span>${this.escapeHtml(item.source)}</span>` : ''}
1198            //                     ${item.dateStr ? `<span>·</span><span>${this.escapeHtml(item.dateStr)}</span>` : ''}
1199            //                     ${item.authors ? `<span>·</span><span>${this.escapeHtml(item.authors.substring(0, 50))}${item.authors.length > 50 ? '...' : ''}</span>` : ''}
1200            //                     ${item.credibility >= 90 ? '<span class="badge-verified"><i class="fas fa-check-circle"></i> Verified</span>' : ''}
1201            //                     ${item.isPeerReviewed ? '<span class="badge-peer-reviewed">Peer-Reviewed</span>' : ''}
1202            //                     ${item.isArxiv ? '<span class="badge-arxiv">arXiv</span>' : ''}
1203            //                     <span class="badge-external">External</span>
1204            //                 </div>
1205            //                 ${item.content ? `<div class="search-result-preview">${this.escapeHtml(item.content)}</div>` : ''}
1206            //             </div>
1207            //             <div class="search-result-score" title="Credibility score">
1208            //                 ${item.credibility}
1209            //             </div>
1210            //         </div>
1211            //     `).join('');
1212            //
1213            //     resultsContainer.innerHTML += externalHTML;
1214            //
1215            //     // Update result count
1216            //     const totalCount = this.results.length + this.externalResults.length;
1217            //     resultCount.textContent = `${totalCount} result${totalCount !== 1 ? 's' : ''} found (${this.externalResults.length} from external sources)`;
1218            //
1219            //     // Add click handlers for external results
1220            //     resultsContainer.querySelectorAll('.external-result').forEach(item => {
1221            //         item.addEventListener('click', () => {
1222            //             const url = item.getAttribute('data-url');
1223            //             window.open(url, '_blank');
1224            //
1225            //             if (window.Analytics) {
1226            //                 Analytics.trackEvent('search-external-click', { source: 'external_api' });
1227            //             }
1228            //         });
1229            //     });
1230            //
1231            //     console.log(`[Enhanced Search] Added ${externalResults.length} external results`);
1232            // } else {
1233            //     console.log('[Enhanced Search] No external results found');
1234            // }
1235        } catch (error) {
1236            console.error('[Enhanced Search] Error fetching external results:', error);
1237            loadingDiv.innerHTML = `
1238                <div class="search-error">
1239                    <i class="fas fa-exclamation-triangle"></i>
1240                    <span>Could not load external results</span>
1241                </div>
1242            `;
1243            setTimeout(() => loadingDiv.remove(), 3000);
1244        }
1245    }
1246
1247    /**
1248     * Create snippet with context around matched terms
1249     */
1250    createSnippet(text, query, maxLength = 150) {
1251        const queryLower = query.toLowerCase();
1252        const textLower = text.toLowerCase();
1253
1254        // Find first occurrence of any query word
1255        const words = queryLower.split(/\s+/);
1256        let matchPos = -1;
1257
1258        for (const word of words) {
1259            const pos = textLower.indexOf(word);
1260            if (pos !== -1) {
1261                matchPos = pos;
1262                break;
1263            }
1264        }
1265
1266        if (matchPos === -1) {
1267            // No match found, return start of text
1268            return this.escapeHtml(text.substring(0, maxLength)) + (text.length > maxLength ? '...' : '');
1269        }
1270
1271        // Extract snippet around match
1272        const start = Math.max(0, matchPos - 50);
1273        const end = Math.min(text.length, matchPos + maxLength - 50);
1274        let snippet = text.substring(start, end);
1275
1276        if (start > 0) snippet = '...' + snippet;
1277        if (end < text.length) snippet = snippet + '...';
1278
1279        return this.highlightMatch(snippet, query);
1280    }
1281
1282    /**
1283     * Navigate through results with arrow keys
1284     */
1285    navigateResults(direction) {
1286        if (this.results.length === 0) return;
1287
1288        this.currentResultIndex += direction;
1289
1290        if (this.currentResultIndex < 0) {
1291            this.currentResultIndex = this.results.length - 1;
1292        } else if (this.currentResultIndex >= this.results.length) {
1293            this.currentResultIndex = 0;
1294        }
1295
1296        this.setActiveResult(this.currentResultIndex);
1297    }
1298
1299    /**
1300     * Set active result
1301     */
1302    setActiveResult(index) {
1303        const items = document.querySelectorAll('.search-result-item');
1304        items.forEach((item, i) => {
1305            if (i === index) {
1306                item.classList.add('active');
1307                item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
1308            } else {
1309                item.classList.remove('active');
1310            }
1311        });
1312        this.currentResultIndex = index;
1313    }
1314
1315    /**
1316     * Select current result
1317     */
1318    selectResult() {
1319        const activeItem = document.querySelector('.search-result-item.active');
1320        if (activeItem) {
1321            const query = document.getElementById('global-search-input').value;
1322            this.addToHistory(query);
1323            activeItem.click();
1324        }
1325    }
1326
1327    /**
1328     * Open search
1329     */
1330    openSearch() {
1331        const overlay = document.getElementById('search-overlay');
1332        const input = document.getElementById('global-search-input');
1333
1334        overlay.classList.add('active');
1335        document.body.style.overflow = 'hidden';
1336
1337        // Focus input after animation
1338        setTimeout(() => {
1339            input.focus();
1340            // Show search history
1341            this.handleSearch('');
1342        }, 100);
1343
1344        // Announce to screen readers
1345        if (window.ARIAHelpers) {
1346            ARIAHelpers.announce('Search opened');
1347        }
1348    }
1349
1350    /**
1351     * Close search
1352     */
1353    closeSearch() {
1354        const overlay = document.getElementById('search-overlay');
1355        const input = document.getElementById('global-search-input');
1356        const clearBtn = document.querySelector('.search-clear');
1357        const resultCount = document.getElementById('search-result-count');
1358
1359        overlay.classList.remove('active');
1360        document.body.style.overflow = '';
1361        input.value = '';
1362        clearBtn.style.display = 'none';
1363        document.getElementById('search-results').innerHTML = '';
1364        resultCount.style.display = 'none';
1365
1366        // Reset filters
1367        this.filters.type = 'all';
1368        document.querySelectorAll('.filter-btn').forEach(btn => {
1369            btn.classList.toggle('active', btn.getAttribute('data-filter-type') === 'all');
1370        });
1371
1372        // Announce to screen readers
1373        if (window.ARIAHelpers) {
1374            ARIAHelpers.announce('Search closed');
1375        }
1376    }
1377
1378    /**
1379     * Render empty search state
1380     */
1381    renderSearchHistory() {
1382        return `
1383            <div class="search-empty">
1384                <i class="fas fa-search" aria-hidden="true"></i>
1385                <p>Search results will appear here</p>
1386            </div>
1387        `;
1388    }
1389
1390    /**
1391     * Add query to search history (disabled)
1392     */
1393    addToHistory(query) {
1394        // Search history functionality disabled
1395        return;
1396    }
1397
1398    /**
1399     * Highlight matching text
1400     */
1401    highlightMatch(text, query) {
1402        if (!query) return this.escapeHtml(text);
1403
1404        const words = query.toLowerCase().split(/\s+/);
1405        let highlighted = this.escapeHtml(text);
1406
1407        words.forEach(word => {
1408            const regex = new RegExp(`(${this.escapeRegex(word)})`, 'gi');
1409            highlighted = highlighted.replace(regex, '<mark>$1</mark>');
1410        });
1411
1412        return highlighted;
1413    }
1414
1415    /**
1416     * Escape HTML
1417     */
1418    escapeHtml(text) {
1419        const div = document.createElement('div');
1420        div.textContent = text;
1421        return div.innerHTML;
1422    }
1423
1424    /**
1425     * Escape regex special characters
1426     */
1427    escapeRegex(str) {
1428        return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1429    }
1430
1431    /**
1432     * Get icon for content type
1433     */
1434    getIcon(type) {
1435        const icons = {
1436            'article': 'newspaper',
1437            'gallery': 'images',
1438            'publication': 'atom',
1439            'faq': 'question-circle'
1440        };
1441        return icons[type] || 'file';
1442    }
1443
1444    /**
1445     * Get display label for content type
1446     */
1447    getTypeLabel(type) {
1448        const labels = {
1449            'article': 'Article',
1450            'gallery': 'Gallery',
1451            'publication': 'Research Paper',
1452            'faq': 'FAQ'
1453        };
1454        return labels[type] || type;
1455    }
1456}
1457
1458// Initialize on page load
1459if (document.readyState === 'loading') {
1460    document.addEventListener('DOMContentLoaded', () => {
1461        window.GlobalSearch = new EnhancedGlobalSearch();
1462    });
1463} else {
1464    window.GlobalSearch = new EnhancedGlobalSearch();
1465}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.