11import { distance , closest } from 'fastest-levenshtein' ;
22import { memo } from './server.caching' ;
33
4- /**
5- * normalizeString function interface
6- */
7- interface NormalizeString {
8- ( str : unknown ) : string ;
9- memo : ( str : unknown ) => string ;
10- }
11-
124/**
135 * Options for closest search
6+ *
7+ * @interface ClosestSearchOptions
8+ *
9+ * @property missingReturnValue - The value to return when no match is found.
10+ * @property normalizeFn - Function to normalize strings for comparison.
1411 */
1512interface ClosestSearchOptions {
1613 missingReturnValue ?: unknown ;
@@ -53,17 +50,25 @@ interface FuzzySearch {
5350/**
5451 * Options for fuzzy search
5552 *
56- * - `maxDistance` - Maximum edit distance for a match. Distance is defined as
53+ * @interface FuzzySearchOptions
54+ *
55+ * @param allowEmptyQuery - Allow empty queries to match items with length <= maxDistance (default: `false`)
56+ * @param maxDistance - Maximum edit distance for a match. Distance is defined as
5757 * - exact = 0
5858 * - prefix = 1
5959 * - suffix = 1
6060 * - contains = 2
6161 * - partial = 2
6262 * - fuzzy = Levenshtein edit distance
63- * - `maxResults` - Maximum number of results to return
64- * - `normalizeFn` - Function to normalize values (default: `normalizeString`)
65- * - `isExactMatch` | `isPrefixMatch` | `isSuffixMatch` | `isContainsMatch` | `isFuzzyMatch` - Enable specific match modes
66- * - `deduplicateByNormalized` - If true, deduplicate results by normalized value instead of original string (default: false)
63+ * @param maxResults - Maximum number of results to return
64+ * @param normalizeFn - Function to normalize strings. Should always return a string (default: `normalizeString`)
65+ * @param isExactMatch - Include exact matches in results (default: `true`)
66+ * @param isPrefixMatch - Include prefix matches in results (default: `true`)
67+ * @param isSuffixMatch - Include suffix matches in results (default: `true`)
68+ * @param isContainsMatch - Include contains matches in results (default: `true`)
69+ * @param isPartialMatch - Include partial matches in results (default: `true`)
70+ * @param isFuzzyMatch - Allow fuzzy matches even when `maxDistance` is negative or zero.
71+ * @param deduplicateByNormalized - If `true`, deduplicate results by normalized value instead of original value.
6772 */
6873interface FuzzySearchOptions {
6974 allowEmptyQuery ?: boolean ;
@@ -80,16 +85,17 @@ interface FuzzySearchOptions {
8085}
8186
8287/**
83- * Internal lightweight normalization: trim, lowercase, remove diacritics (a sign/accent character), squash separators
88+ * Internal lightweight normalization: coerce any value to string, trim, lowercase,
89+ * remove diacritics (a sign/accent character), squash separators.
8490 *
8591 * - Functions `findClosest` and `fuzzySearch` use this internally.
8692 * - Can be overridden in the `findClosest` and `fuzzySearch` related options for custom normalization.
8793 * - Function has a `memo` property to allow use as a memoized function.
8894 *
89- * @param str - String or number to normalize.
90- * @returns Normalized or empty string
95+ * @param str - Any value to normalize.
96+ * @returns Normalized string
9197 */
92- const normalizeString : NormalizeString = ( str : unknown ) => String ( str )
98+ const normalizeString = ( str : unknown ) => String ( str )
9399 . trim ( )
94100 . toLowerCase ( )
95101 . normalize ( 'NFKD' )
@@ -108,9 +114,9 @@ normalizeString.memo = memo(normalizeString, { cacheLimit: 50 });
108114 * - Returns the **first** original item whose normalized value equals the best normalized candidate.
109115 * - If multiple items normalize to the same value, only the first occurrence in the array is returned.
110116 * - For multiple matches, use `fuzzySearch` instead.
111- * - Null/undefined items are normalized to empty strings to prevent runtime errors .
117+ * - Null/undefined items are coerced to strings to make them searchable .
112118 *
113- * @param query - Search query string or number
119+ * @param query - Search query value
114120 * @param items - Array of strings and/or numbers to search
115121 * @param {ClosestSearchOptions } options - Search configuration options
116122 * @returns Closest matching item from items.
@@ -151,20 +157,9 @@ const findClosest = (
151157 * - Negative `maxDistance` values intentionally filter out all results, including exact matches.
152158 * - Empty-query fallback is allowed when `isFuzzyMatch` is true (items with length <= maxDistance can match).
153159 *
154- * @param query - Search query string or number
160+ * @param query - Search query value
155161 * @param items - Array of strings and/or numbers to search
156162 * @param {FuzzySearchOptions } options - Search configuration options
157- * @param options.allowEmptyQuery - Allow empty queries to match items with length <= maxDistance (default: `false`)
158- * @param options.maxDistance - Maximum edit distance for a match. Distance is defined as
159- * @param options.maxResults - Maximum number of results to return
160- * @param {NormalizeString } options.normalizeFn - Function to normalize strings. Should always return a string or empty string (default: `normalizeString`)
161- * @param options.isExactMatch - Include exact matches in results (default: `true`)
162- * @param options.isPrefixMatch - Include prefix matches in results (default: `true`)
163- * @param options.isSuffixMatch - Include suffix matches in results (default: `true`)
164- * @param options.isContainsMatch - Include contains matches in results (default: `true`)
165- * @param options.isPartialMatch - Include partial matches in results (default: `true`)
166- * @param options.isFuzzyMatch - Allow fuzzy matches even when `maxDistance` is negative or zero.
167- * @param options.deduplicateByNormalized - If `true`, deduplicate results by normalized value instead of original string.
168163 * @returns {FuzzySearch } An object containing search results with distance and match type
169164 * - `results`: Array of matching strings with distance and match type.
170165 * - `totalResults`: Total number of results found.
@@ -226,13 +221,13 @@ const fuzzySearch = (
226221 } else if ( normalizedQuery !== '' && normalizedItem !== '' && normalizedQuery . includes ( normalizedItem ) ) {
227222 matchType = 'partial' ;
228223 editDistance = 2 ;
229- } else if ( isFuzzyMatch && ( allowEmptyQuery || ( normalizedQuery !== '' && normalizedItem !== '' ) ) ) {
230- const checkDistance = distance ( normalizedItem , normalizedQuery ) ;
231-
232- if ( checkDistance <= maxDistance ) {
233- matchType = 'fuzzy' ;
234- editDistance = checkDistance ;
235- }
224+ } else if (
225+ isFuzzyMatch &&
226+ ( allowEmptyQuery || ( normalizedQuery !== '' && normalizedItem !== '' ) ) &&
227+ Math . abs ( normalizedItem . length - normalizedQuery . length ) <= maxDistance
228+ ) {
229+ matchType = 'fuzzy' ;
230+ editDistance = distance ( normalizedItem , normalizedQuery ) ;
236231 }
237232
238233 if ( matchType === undefined ) {
@@ -275,7 +270,6 @@ export {
275270 normalizeString ,
276271 fuzzySearch ,
277272 findClosest ,
278- type NormalizeString ,
279273 type ClosestSearchOptions ,
280274 type FuzzySearch ,
281275 type FuzzySearchResult ,
0 commit comments