@@ -1952,128 +1952,134 @@ for (cEmoji of arrEmojis) {
19521952 */
19531953function searchEmojis ( search ) {
19541954 if ( ! search || search . length < 1 ) return [ ] ;
1955-
1955+
19561956 search = search . toLowerCase ( ) ;
19571957 const results = [ ] ;
1958-
1958+
19591959 for ( const emojiData of arrEmojis ) {
19601960 const name = emojiData . name . toLowerCase ( ) ;
1961+ const display = ( emojiData . display || '' ) . toLowerCase ( ) ;
19611962 const words = name . split ( ' ' ) ;
1962-
1963+
19631964 let bestMatch = null ;
1964-
1965- for ( const word of words ) {
1966- // Exact match - highest priority
1965+ const consider = ( score , word , wordIndex ) => {
1966+ if ( ! bestMatch || score < bestMatch . score ) {
1967+ bestMatch = { score, word, wordIndex } ;
1968+ }
1969+ } ;
1970+
1971+ // Display-name tier — strongest signal (search "fire" → 🔥 "fire" wins)
1972+ if ( display === search ) {
1973+ consider ( - 1.0 , display , 0 ) ;
1974+ } else if ( display . startsWith ( search + ' ' ) ) {
1975+ consider ( - 0.5 , display , 0 ) ;
1976+ }
1977+
1978+ for ( let i = 0 ; i < words . length ; i ++ ) {
1979+ const word = words [ i ] ;
1980+
1981+ // Exact word match — first-word matches outrank later positions.
1982+ // Without this, "fire" matches both 🔥 (word 0) and ❤️🔥 (word 1)
1983+ // identically; the emoji at word position 0 is the more specific
1984+ // referent and should rank first.
19671985 if ( word === search ) {
1968- bestMatch = { score : 0 , word } ;
1969- break ;
1986+ consider ( 0 + i * 0.02 , word , i ) ;
1987+ continue ;
19701988 }
1971-
1989+
19721990 // Word starts with search
19731991 if ( word . startsWith ( search ) && search . length >= 2 ) {
1974- const score = 0.1 ;
1975- if ( ! bestMatch || score < bestMatch . score ) {
1976- bestMatch = { score, word } ;
1977- }
1992+ consider ( 0.1 + i * 0.02 , word , i ) ;
19781993 }
1979-
1994+
19801995 // Fuzzy matching for 3+ character searches
19811996 if ( search . length >= 3 ) {
19821997 const lengthDiff = Math . abs ( word . length - search . length ) ;
1983-
1998+
19841999 // Check words with similar length
19852000 if ( lengthDiff <= 1 ) {
19862001 const distance = levenshteinDistance ( word , search ) ;
1987-
2002+
19882003 if ( distance === 1 ) {
19892004 // Verify sufficient character overlap
19902005 const searchChars = new Set ( search ) ;
19912006 const wordChars = new Set ( word ) ;
19922007 const commonChars = [ ...searchChars ] . filter ( c => wordChars . has ( c ) ) . length ;
1993-
2008+
19942009 if ( commonChars >= search . length - 1 ) {
19952010 let score = 0.5 ;
1996-
1997- // Better score for missing letter at end (hart → heart)
19982011 if ( word . length > search . length && word . startsWith ( search ) ) {
1999- score = 0.3 ;
2000- }
2001- // Better score for missing letter in middle
2002- else if ( word . length > search . length &&
2003- word [ 0 ] === search [ 0 ] &&
2012+ score = 0.3 ; // missing letter at end (hart → heart)
2013+ } else if ( word . length > search . length &&
2014+ word [ 0 ] === search [ 0 ] &&
20042015 word [ word . length - 1 ] === search [ search . length - 1 ] ) {
2005- score = 0.35 ;
2006- }
2007-
2008- if ( ! bestMatch || score < bestMatch . score ) {
2009- bestMatch = { score, word } ;
2016+ score = 0.35 ; // missing letter in middle
20102017 }
2018+ consider ( score + i * 0.02 , word , i ) ;
20112019 }
20122020 }
20132021 }
2014-
2022+
20152023 // Check if search matches beginning of longer words
20162024 if ( word . length > search . length && lengthDiff <= 3 ) {
20172025 const wordStart = word . substring ( 0 , search . length ) ;
20182026 const distance = levenshteinDistance ( wordStart , search ) ;
2019-
2027+
20202028 if ( distance === 1 ) {
2021- const score = 0.6 ;
2022- if ( ! bestMatch || score < bestMatch . score ) {
2023- bestMatch = { score, word } ;
2024- }
2029+ consider ( 0.6 + i * 0.02 , word , i ) ;
20252030 }
20262031 }
20272032 }
2028-
2033+
20292034 // For 5+ character searches, allow distance 2
20302035 if ( search . length >= 5 ) {
20312036 const distance = levenshteinDistance ( word , search ) ;
20322037 if ( distance === 2 && Math . abs ( word . length - search . length ) <= 1 ) {
2033- const score = 0.8 ;
2034- if ( ! bestMatch || score < bestMatch . score ) {
2035- bestMatch = { score, word } ;
2036- }
2038+ consider ( 0.8 + i * 0.02 , word , i ) ;
20372039 }
20382040 }
20392041 }
2040-
2042+
20412043 if ( bestMatch ) {
20422044 results . push ( {
20432045 ...emojiData ,
20442046 score : bestMatch . score ,
2045- matchedWord : bestMatch . word
2047+ matchedWord : bestMatch . word ,
2048+ matchedIndex : bestMatch . wordIndex
20462049 } ) ;
20472050 }
20482051 }
2049-
2052+
20502053 // If no results found, fall back to includes search
20512054 if ( results . length === 0 ) {
20522055 for ( const emojiData of arrEmojis ) {
20532056 const name = emojiData . name . toLowerCase ( ) ;
20542057 if ( name . includes ( search ) ) {
20552058 results . push ( {
20562059 ...emojiData ,
2057- score : 1.0 , // Lower priority than fuzzy matches
2058- matchedWord : name
2060+ score : 1.0 ,
2061+ matchedWord : name ,
2062+ matchedIndex : 99
20592063 } ) ;
20602064 }
20612065 }
20622066 }
2063-
2067+
20642068 // Sort by relevance
20652069 return results
20662070 . sort ( ( a , b ) => {
20672071 if ( Math . abs ( a . score - b . score ) > 0.01 ) return a . score - b . score ;
2068-
2069- // For same scores, prefer shorter matched words
2070- const aLen = a . matchedWord . length ;
2071- const bLen = b . matchedWord . length ;
2072- if ( aLen !== bLen ) return aLen - bLen ;
2073-
2072+ // Earlier-position matches first (primary keyword > secondary)
2073+ if ( a . matchedIndex !== b . matchedIndex ) return a . matchedIndex - b . matchedIndex ;
2074+ // Shorter matched word is more specific
2075+ if ( a . matchedWord . length !== b . matchedWord . length ) return a . matchedWord . length - b . matchedWord . length ;
2076+ // Shorter total name is more specific (fire-engine beats fire-engine-with-bells)
2077+ const aNameLen = a . name ? a . name . length : 0 ;
2078+ const bNameLen = b . name ? b . name . length : 0 ;
2079+ if ( aNameLen !== bNameLen ) return aNameLen - bNameLen ;
20742080 return a . matchedWord . localeCompare ( b . matchedWord ) ;
20752081 } )
2076- . map ( ( { score, matchedWord, ...emoji } ) => emoji ) ;
2082+ . map ( ( { score, matchedWord, matchedIndex , ...emoji } ) => emoji ) ;
20772083}
20782084
20792085/** Return our most used emojis */
0 commit comments