@@ -112,39 +112,131 @@ async function search(query, opt) {
112112 jsonDataIsCombined = false ;
113113 }
114114
115- let results = { } ;
116-
117- query = query . toLowerCase ( ) . replace ( / \s / gm, '' ) ;
115+ // Properties to search through for each chip/object:
116+ // - key (This is the chip/object name without spaces)
117+ // - chipName (The actual chip/object name with spaces, as it appears in the data)
118+ // - paletteName (The chip/object name as it appears in the palette, which may differ from chipName for some chips/objects)
119+ // - description (The chip/object description, which may contain the chip/object name or related keywords)
120+
121+ const results = { } ;
122+
123+ const qRaw = String ( query ?? '' ) . trim ( ) ;
124+ const normalizeKey = ( s ) => String ( s ?? '' ) . toLowerCase ( ) . replace ( / \s + / g, '' ) ;
125+ const normalizeText = ( s ) => String ( s ?? '' )
126+ . toLowerCase ( )
127+ . replace ( / [ _ \- ] + / g, ' ' )
128+ . replace ( / [ ^ a - z 0 - 9 ] + / g, ' ' )
129+ . replace ( / \s + / g, ' ' )
130+ . trim ( ) ;
131+ const tokenize = ( s ) => {
132+ const t = normalizeText ( s ) ;
133+ return t ? t . split ( ' ' ) : [ ] ;
134+ } ;
135+
136+ const qKey = normalizeKey ( qRaw ) ;
137+ const qText = normalizeText ( qRaw ) ;
138+ const qTokens = tokenize ( qRaw ) ;
139+ const isShort = qKey . length <= 2 ;
140+ if ( ! qKey ) return { } ;
141+
142+ const scoreCandidate = ( q , candidate , weight ) => {
143+ if ( ! candidate ) return 0 ;
144+ const cText = normalizeText ( candidate ) ;
145+ const cKey = normalizeKey ( candidate ) ;
146+ if ( ! cText && ! cKey ) return 0 ;
147+
148+ // Exact matches should always win.
149+ if ( cKey === qKey ) return weight + 100000 ;
150+
151+ const cTokens = tokenize ( candidate ) ;
152+ const hasTokenExact = cTokens . includes ( qText ) || cTokens . includes ( qKey ) ;
153+ const hasTokenPrefix = qTokens . length > 0
154+ ? qTokens . every ( qt => cTokens . some ( ct => ct . startsWith ( qt ) ) )
155+ : cTokens . some ( ct => ct . startsWith ( qText ) ) ;
156+
157+ let score = 0 ;
158+
159+ // For very short queries (like "if"), avoid substring matches inside other words.
160+ if ( isShort ) {
161+ if ( hasTokenExact ) score = Math . max ( score , weight + 60000 ) ;
162+ if ( cTokens . some ( t => t . startsWith ( qKey ) ) ) score = Math . max ( score , weight + 50000 ) ;
163+ if ( cText . startsWith ( qText ) ) score = Math . max ( score , weight + 45000 ) ;
164+ return score ;
165+ }
166+
167+ if ( hasTokenExact ) score = Math . max ( score , weight + 65000 ) ;
168+ if ( cText . startsWith ( qText ) || cKey . startsWith ( qKey ) ) score = Math . max ( score , weight + 55000 ) ;
169+ if ( hasTokenPrefix ) score = Math . max ( score , weight + 45000 ) ;
170+ if ( cText . includes ( qText ) || cKey . includes ( qKey ) ) score = Math . max ( score , weight + 35000 ) ;
171+
172+ // Light fuzzy boost: trigram overlap on the space-stripped forms.
173+ const tri = ( s ) => {
174+ const ss = String ( s || '' ) ;
175+ if ( ss . length < 3 ) return new Set ( [ ss ] ) ;
176+ const set = new Set ( ) ;
177+ for ( let i = 0 ; i <= ss . length - 3 ; i ++ ) set . add ( ss . slice ( i , i + 3 ) ) ;
178+ return set ;
179+ } ;
180+ const a = tri ( qKey ) ;
181+ const b = tri ( cKey ) ;
182+ let inter = 0 ;
183+ for ( const x of a ) if ( b . has ( x ) ) inter ++ ;
184+ const union = a . size + b . size - inter ;
185+ const jacc = union > 0 ? inter / union : 0 ;
186+ if ( jacc > 0 ) score += Math . floor ( jacc * 5000 ) ;
187+
188+ return score ;
189+ } ;
190+
191+ const scoreChip = ( chipName , chipData ) => {
192+ const name = chipData ?. chipName || chipName ;
193+ const paletteName = chipData ?. paletteName || '' ;
194+ const description = chipData ?. description || '' ;
195+
196+ // Search fields listed in your comment header (with different weights).
197+ const byKey = scoreCandidate ( qKey , chipName , 2000 ) ;
198+ const byChipName = scoreCandidate ( qKey , name , 3000 ) ;
199+ const byPalette = scoreCandidate ( qKey , paletteName , 1500 ) ;
200+ const byDesc = scoreCandidate ( qKey , description , 500 ) ;
201+ return Math . max ( byKey , byChipName , byPalette , byDesc ) ;
202+ } ;
118203
119204 if ( jsonDataIsCombined ) {
205+ const matches = [ ] ;
120206 $ . each ( jsonData , function ( chipName , chipData ) {
121- if ( chipName . toLowerCase ( ) . replace ( / \s / gm, '' ) . match ( new RegExp ( `${ query } ` , 'gm' ) ) ) {
122- results [ chipName ] = chipData ;
123- }
207+ const score = scoreChip ( chipName , chipData ) ;
208+ if ( score > 0 ) matches . push ( { chipName, chipData, score } ) ;
124209 } ) ;
125- } else {
126- $ . each ( jsonData , function ( key , value ) {
210+ matches . sort ( ( a , b ) => ( b . score - a . score ) || a . chipName . localeCompare ( b . chipName ) ) ;
211+ for ( const m of matches ) results [ m . chipName ] = m . chipData ;
212+ return results ;
213+ }
214+
215+ if ( options . combineResults ) {
216+ const matches = [ ] ;
217+ $ . each ( jsonData , function ( groupKey , value ) {
127218 $ . each ( value , function ( chipName , chipData ) {
128- if ( chipName . toLowerCase ( ) . replace ( / \s / gm, '' ) . match ( new RegExp ( `${ query } ` , 'gm' ) ) ) {
129- if ( options . combineResults ) {
130- results [ chipName ] = chipData ;
131- return ;
132- }
133- results [ key ] = results [ key ] || { } ;
134- results [ key ] [ chipName ] = chipData ;
135- }
219+ const score = scoreChip ( chipName , chipData ) ;
220+ if ( score > 0 ) matches . push ( { chipName, chipData, score } ) ;
136221 } ) ;
137222 } ) ;
223+ matches . sort ( ( a , b ) => ( b . score - a . score ) || a . chipName . localeCompare ( b . chipName ) ) ;
224+ for ( const m of matches ) results [ m . chipName ] = m . chipData ;
225+ return results ;
138226 }
139227
140- // Sort results alphabetically by chipName within each category (chips/objects) or overall if combined .
141- if ( options . combineResults ) {
142- results = Object . fromEntries ( Object . entries ( results ) . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) ) ) ;
143- } else {
144- $ . each ( results , function ( key , value ) {
145- results [ key ] = Object . fromEntries ( Object . entries ( value ) . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) ) ) ;
228+ // Grouped results: keep group key but order by relevance inside each group .
229+ $ . each ( jsonData , function ( groupKey , value ) {
230+ const matches = [ ] ;
231+ $ . each ( value , function ( chipName , chipData ) {
232+ const score = scoreChip ( chipName , chipData ) ;
233+ if ( score > 0 ) matches . push ( { chipName , chipData , score } ) ;
146234 } ) ;
147- }
235+ if ( matches . length === 0 ) return ;
236+ matches . sort ( ( a , b ) => ( b . score - a . score ) || a . chipName . localeCompare ( b . chipName ) ) ;
237+ results [ groupKey ] = { } ;
238+ for ( const m of matches ) results [ groupKey ] [ m . chipName ] = m . chipData ;
239+ } ) ;
148240
149241 return results ;
150242}
0 commit comments