zsbqb-static 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,311 +1,311 @@
1
- /** 浏览器歌单搜索:与 apps/core/search_index.py 对齐(子串打分 + 模糊回落)。 */
2
- (function (root) {
3
- const FUZZY_MIN_RATIO = 0.72;
4
- const FUZZY_LIMIT = 12;
5
- const NOISE_RE = /[\s\-_/\\|·・'"“”‘’[\]()()【】<>《》,,.。!!??::;;~~]+/g;
6
-
7
- let charPinyin = {};
8
-
9
- function setCharPinyinMap(map) {
10
- charPinyin = map && typeof map === "object" ? map : {};
11
- }
12
-
13
- function compactNoise(value) {
14
- return String(value || "")
15
- .toLowerCase()
16
- .replace(NOISE_RE, "");
17
- }
18
-
19
- function lower(value) {
20
- return String(value || "").toLowerCase();
21
- }
22
-
23
- function queryPinyin(text) {
24
- const compact = compactNoise(text);
25
- if (!compact) return "";
26
- let out = "";
27
- for (const ch of compact) {
28
- out += charPinyin[ch] || ch;
29
- }
30
- return out;
31
- }
32
-
33
- function tokenVariants(token) {
34
- const t = lower(token).trim();
35
- if (!t) return [];
36
- const compact = t.replace(NOISE_RE, "");
37
- const variants = [t];
38
- if (compact && compact !== t) variants.push(compact);
39
- return variants;
40
- }
41
-
42
- function fieldValues(entry, keys) {
43
- const out = [];
44
- keys.forEach((key) => {
45
- const raw = entry[key];
46
- if (raw == null || raw === "") return;
47
- const val = String(raw).toLowerCase();
48
- out.push(val);
49
- const compact = val.replace(NOISE_RE, "");
50
- if (compact !== val) out.push(compact);
51
- });
52
- return out;
53
- }
54
-
55
- const SEARCHABLE_KEYS = [
56
- "title",
57
- "artist",
58
- "title_compact",
59
- "artist_compact",
60
- "title_pinyin",
61
- "artist_pinyin",
62
- "title_initials",
63
- "artist_initials",
64
- "title_first_pinyin",
65
- "artist_first_pinyin",
66
- "title_first_letter",
67
- "artist_first_letter",
68
- ];
69
-
70
- function tokenMatches(entry, token) {
71
- const variants = tokenVariants(token);
72
- if (!variants.length) return true;
73
- const haystack = fieldValues(entry, SEARCHABLE_KEYS);
74
- for (const variant of variants) {
75
- for (const hay of haystack) {
76
- if (hay.includes(variant) || hay.startsWith(variant)) return true;
77
- }
78
- }
79
- return false;
80
- }
81
-
82
- function matchingBlocks(a, b) {
83
- const la = a.length;
84
- const lb = b.length;
85
- const match = new Array(lb);
86
- const blocks = [];
87
- let ai = 0;
88
- while (ai < la) {
89
- let bestJ = -1;
90
- let bestLen = 0;
91
- for (let bj = 0; bj < lb; bj++) {
92
- if (match[bj]) continue;
93
- if (a[ai] !== b[bj]) continue;
94
- let len = 0;
95
- while (ai + len < la && bj + len < lb && !match[bj + len] && a[ai + len] === b[bj + len]) {
96
- len += 1;
97
- }
98
- if (len > bestLen) {
99
- bestLen = len;
100
- bestJ = bj;
101
- }
102
- }
103
- if (bestLen > 0) {
104
- for (let k = 0; k < bestLen; k++) match[bestJ + k] = 1;
105
- blocks.push(bestLen);
106
- ai += bestLen;
107
- } else {
108
- ai += 1;
109
- }
110
- }
111
- return blocks;
112
- }
113
-
114
- function sequenceRatio(left, right) {
115
- if (!left && !right) return 1;
116
- if (!left || !right) return 0;
117
- if (left === right) return 1;
118
- const blocks = matchingBlocks(left, right);
119
- const matches = blocks.reduce((sum, n) => sum + n, 0);
120
- return (2 * matches) / (left.length + right.length);
121
- }
122
-
123
- function similarity(left, right) {
124
- if (!left || !right) return 0;
125
- if (left === right) return 1;
126
- const shorter = left.length <= right.length ? left : right;
127
- const longer = left.length <= right.length ? right : left;
128
- if (shorter.length >= 2 && longer.includes(shorter)) {
129
- return Math.max(0.75, shorter.length / Math.max(longer.length, 1));
130
- }
131
- return sequenceRatio(left, right);
132
- }
133
-
134
- function fuzzyScore(entry, query) {
135
- const needle = lower(query).trim();
136
- if (!needle) return 0;
137
- const compact = needle.replace(NOISE_RE, "");
138
- if (compact.length < 2) return 0;
139
- const needlePy = queryPinyin(needle);
140
- const fields = [
141
- entry.title_compact || String(entry.title || "").toLowerCase().replace(NOISE_RE, ""),
142
- entry.artist_compact || String(entry.artist || "").toLowerCase().replace(NOISE_RE, ""),
143
- entry.title_pinyin || "",
144
- entry.artist_pinyin || "",
145
- ];
146
- if (compact.length <= 6 && /^[\x00-\x7f]+$/.test(compact)) {
147
- fields.push(entry.title_initials || "", entry.artist_initials || "");
148
- }
149
- let best = 0;
150
- fields.forEach((hay) => {
151
- if (!hay || hay.length < 2) return;
152
- best = Math.max(best, similarity(compact, hay));
153
- if (needlePy && needlePy !== compact && needlePy.length >= 2) {
154
- best = Math.max(best, similarity(needlePy, hay));
155
- }
156
- });
157
- const titlePy = entry.title_pinyin || "";
158
- if (needlePy && titlePy && needlePy.length >= 2) {
159
- best = Math.max(best, similarity(needlePy, titlePy));
160
- if (titlePy.startsWith(needlePy) || needlePy.startsWith(titlePy)) {
161
- best = Math.max(best, 0.82);
162
- }
163
- }
164
- if (best < FUZZY_MIN_RATIO) return 0;
165
- return Math.round(best * 100);
166
- }
167
-
168
- function matchScore(entry, query) {
169
- const needle = lower(query).trim();
170
- if (!needle) return 0;
171
- const tokens = needle.split(/\s+/).filter(Boolean);
172
- if (!tokens.length) return 0;
173
- for (const token of tokens) {
174
- if (!tokenMatches(entry, token)) {
175
- const compactToken = token.replace(NOISE_RE, "");
176
- if (!compactToken || !tokenMatches(entry, compactToken)) return 0;
177
- }
178
- }
179
- const needleCompact = needle.replace(NOISE_RE, "");
180
- const title = lower(entry.title);
181
- const artist = lower(entry.artist);
182
- const titleCompact = entry.title_compact || title.replace(NOISE_RE, "");
183
- const artistCompact = entry.artist_compact || artist.replace(NOISE_RE, "");
184
- let score = 10;
185
- if (title === needle || titleCompact === needleCompact) score += 100;
186
- else if (title.startsWith(needle) || titleCompact.startsWith(needleCompact)) score += 60;
187
- else if (title.includes(needle) || titleCompact.includes(needleCompact)) score += 40;
188
- if (artist === needle || artistCompact === needleCompact) score += 50;
189
- else if (artist.includes(needle) || artistCompact.includes(needleCompact)) score += 25;
190
- const titlePinyin = entry.title_pinyin || "";
191
- const artistPinyin = entry.artist_pinyin || "";
192
- const needlePy = queryPinyin(needle);
193
- if (
194
- needleCompact &&
195
- (titlePinyin.startsWith(needleCompact) ||
196
- artistPinyin.startsWith(needleCompact) ||
197
- titlePinyin.includes(needleCompact) ||
198
- artistPinyin.includes(needleCompact))
199
- ) {
200
- score += 20;
201
- }
202
- if (
203
- needlePy &&
204
- needlePy !== needleCompact &&
205
- (titlePinyin.startsWith(needlePy) ||
206
- titlePinyin.includes(needlePy) ||
207
- artistPinyin.startsWith(needlePy) ||
208
- artistPinyin.includes(needlePy))
209
- ) {
210
- score += 35;
211
- }
212
- const titleInitials = entry.title_initials || "";
213
- const artistInitials = entry.artist_initials || "";
214
- if (
215
- needleCompact &&
216
- (titleInitials.startsWith(needleCompact) ||
217
- artistInitials.startsWith(needleCompact) ||
218
- titleInitials.includes(needleCompact) ||
219
- artistInitials.includes(needleCompact))
220
- ) {
221
- score += 15;
222
- }
223
- if (tokens.length === 1 && tokens[0].length === 1) {
224
- const token = tokens[0];
225
- if (token === entry.title_first_letter || token === entry.artist_first_letter) score += 12;
226
- }
227
- return score;
228
- }
229
-
230
- function bpmMatches(entry, bpmBucket, bpmExact) {
231
- const raw = entry.bpm;
232
- const bpm = Number(raw);
233
- const hasBpm = raw !== "" && raw != null && Number.isFinite(bpm);
234
- if (bpmExact || bpmBucket) {
235
- if (!hasBpm) return false;
236
- if (bpmExact) return Math.round(bpm) === parseInt(bpmExact, 10);
237
- if (bpmBucket === "100-") return bpm < 100;
238
- if (bpmBucket === "200+") return bpm >= 200;
239
- const low = parseInt(bpmBucket, 10);
240
- return bpm >= low && bpm < low + 10;
241
- }
242
- return true;
243
- }
244
-
245
- function letterMatches(entry, letter) {
246
- const key = String(letter || "").trim();
247
- if (!key) return true;
248
- if (key === "#") return Boolean(entry.title_lead_non_zh_en);
249
- return String(entry.title_first_letter || "").toLowerCase() === key.toLowerCase();
250
- }
251
-
252
- function titleSortKey(title) {
253
- const text = String(title || "");
254
- const ch = text.trim()[0] || "";
255
- const nonZhEn = ch && !/[A-Za-z]/.test(ch) && !/[\u4e00-\u9fff]/.test(ch);
256
- return [nonZhEn ? 0 : 1, text];
257
- }
258
-
259
- function searchCatalog(songs, query, filters) {
260
- const list = Array.isArray(songs) ? songs : [];
261
- const needle = String(query || "").trim();
262
- const star = String((filters && filters.star) || "").trim();
263
- const letter = String((filters && filters.letter) || "").trim();
264
- const bpmBucket = String((filters && filters.bpmBucket) || "").trim();
265
- const bpmExact = (filters && filters.bpmExact) || "";
266
- const exact = [];
267
- list.forEach((entry) => {
268
- if (star && String(entry.star) !== star) return;
269
- if (!letterMatches(entry, letter)) return;
270
- if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
271
- if (!needle) {
272
- exact.push({ entry, score: 0, fuzzy: false });
273
- return;
274
- }
275
- const score = matchScore(entry, needle);
276
- if (score > 0) exact.push({ entry, score, fuzzy: false });
277
- });
278
- let ranked = exact;
279
- if (needle && !ranked.length) {
280
- const fuzzy = [];
281
- list.forEach((entry) => {
282
- if (star && String(entry.star) !== star) return;
283
- if (!letterMatches(entry, letter)) return;
284
- if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
285
- const score = fuzzyScore(entry, needle);
286
- if (score > 0) fuzzy.push({ entry, score, fuzzy: true });
287
- });
288
- fuzzy.sort((a, b) => b.score - a.score || titleSortKey(a.entry.title).join("").localeCompare(titleSortKey(b.entry.title).join("")));
289
- ranked = fuzzy.slice(0, FUZZY_LIMIT);
290
- } else if (needle) {
291
- ranked.sort((a, b) => b.score - a.score || String(a.entry.title).localeCompare(String(b.entry.title), "zh"));
292
- } else {
293
- ranked.sort((a, b) => {
294
- const ka = titleSortKey(a.entry.title);
295
- const kb = titleSortKey(b.entry.title);
296
- if (ka[0] !== kb[0]) return ka[0] - kb[0];
297
- return String(ka[1]).localeCompare(String(kb[1]), "zh");
298
- });
299
- }
300
- return ranked;
301
- }
302
-
303
- root.CatalogSearch = {
304
- setCharPinyinMap,
305
- queryPinyin,
306
- matchScore,
307
- fuzzyScore,
308
- searchCatalog,
309
- FUZZY_LIMIT,
310
- };
311
- })(typeof window !== "undefined" ? window : globalThis);
1
+ /** 浏览器歌单搜索:与 apps/core/search_index.py 对齐(子串打分 + 模糊回落)。 */
2
+ (function (root) {
3
+ const FUZZY_MIN_RATIO = 0.72;
4
+ const FUZZY_LIMIT = 12;
5
+ const NOISE_RE = /[\s\-_/\\|·・'"“”‘’[\]()()【】<>《》,,.。!!??::;;~~]+/g;
6
+
7
+ let charPinyin = {};
8
+
9
+ function setCharPinyinMap(map) {
10
+ charPinyin = map && typeof map === "object" ? map : {};
11
+ }
12
+
13
+ function compactNoise(value) {
14
+ return String(value || "")
15
+ .toLowerCase()
16
+ .replace(NOISE_RE, "");
17
+ }
18
+
19
+ function lower(value) {
20
+ return String(value || "").toLowerCase();
21
+ }
22
+
23
+ function queryPinyin(text) {
24
+ const compact = compactNoise(text);
25
+ if (!compact) return "";
26
+ let out = "";
27
+ for (const ch of compact) {
28
+ out += charPinyin[ch] || ch;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function tokenVariants(token) {
34
+ const t = lower(token).trim();
35
+ if (!t) return [];
36
+ const compact = t.replace(NOISE_RE, "");
37
+ const variants = [t];
38
+ if (compact && compact !== t) variants.push(compact);
39
+ return variants;
40
+ }
41
+
42
+ function fieldValues(entry, keys) {
43
+ const out = [];
44
+ keys.forEach((key) => {
45
+ const raw = entry[key];
46
+ if (raw == null || raw === "") return;
47
+ const val = String(raw).toLowerCase();
48
+ out.push(val);
49
+ const compact = val.replace(NOISE_RE, "");
50
+ if (compact !== val) out.push(compact);
51
+ });
52
+ return out;
53
+ }
54
+
55
+ const SEARCHABLE_KEYS = [
56
+ "title",
57
+ "artist",
58
+ "title_compact",
59
+ "artist_compact",
60
+ "title_pinyin",
61
+ "artist_pinyin",
62
+ "title_initials",
63
+ "artist_initials",
64
+ "title_first_pinyin",
65
+ "artist_first_pinyin",
66
+ "title_first_letter",
67
+ "artist_first_letter",
68
+ ];
69
+
70
+ function tokenMatches(entry, token) {
71
+ const variants = tokenVariants(token);
72
+ if (!variants.length) return true;
73
+ const haystack = fieldValues(entry, SEARCHABLE_KEYS);
74
+ for (const variant of variants) {
75
+ for (const hay of haystack) {
76
+ if (hay.includes(variant) || hay.startsWith(variant)) return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+
82
+ function matchingBlocks(a, b) {
83
+ const la = a.length;
84
+ const lb = b.length;
85
+ const match = new Array(lb);
86
+ const blocks = [];
87
+ let ai = 0;
88
+ while (ai < la) {
89
+ let bestJ = -1;
90
+ let bestLen = 0;
91
+ for (let bj = 0; bj < lb; bj++) {
92
+ if (match[bj]) continue;
93
+ if (a[ai] !== b[bj]) continue;
94
+ let len = 0;
95
+ while (ai + len < la && bj + len < lb && !match[bj + len] && a[ai + len] === b[bj + len]) {
96
+ len += 1;
97
+ }
98
+ if (len > bestLen) {
99
+ bestLen = len;
100
+ bestJ = bj;
101
+ }
102
+ }
103
+ if (bestLen > 0) {
104
+ for (let k = 0; k < bestLen; k++) match[bestJ + k] = 1;
105
+ blocks.push(bestLen);
106
+ ai += bestLen;
107
+ } else {
108
+ ai += 1;
109
+ }
110
+ }
111
+ return blocks;
112
+ }
113
+
114
+ function sequenceRatio(left, right) {
115
+ if (!left && !right) return 1;
116
+ if (!left || !right) return 0;
117
+ if (left === right) return 1;
118
+ const blocks = matchingBlocks(left, right);
119
+ const matches = blocks.reduce((sum, n) => sum + n, 0);
120
+ return (2 * matches) / (left.length + right.length);
121
+ }
122
+
123
+ function similarity(left, right) {
124
+ if (!left || !right) return 0;
125
+ if (left === right) return 1;
126
+ const shorter = left.length <= right.length ? left : right;
127
+ const longer = left.length <= right.length ? right : left;
128
+ if (shorter.length >= 2 && longer.includes(shorter)) {
129
+ return Math.max(0.75, shorter.length / Math.max(longer.length, 1));
130
+ }
131
+ return sequenceRatio(left, right);
132
+ }
133
+
134
+ function fuzzyScore(entry, query) {
135
+ const needle = lower(query).trim();
136
+ if (!needle) return 0;
137
+ const compact = needle.replace(NOISE_RE, "");
138
+ if (compact.length < 2) return 0;
139
+ const needlePy = queryPinyin(needle);
140
+ const fields = [
141
+ entry.title_compact || String(entry.title || "").toLowerCase().replace(NOISE_RE, ""),
142
+ entry.artist_compact || String(entry.artist || "").toLowerCase().replace(NOISE_RE, ""),
143
+ entry.title_pinyin || "",
144
+ entry.artist_pinyin || "",
145
+ ];
146
+ if (compact.length <= 6 && /^[\x00-\x7f]+$/.test(compact)) {
147
+ fields.push(entry.title_initials || "", entry.artist_initials || "");
148
+ }
149
+ let best = 0;
150
+ fields.forEach((hay) => {
151
+ if (!hay || hay.length < 2) return;
152
+ best = Math.max(best, similarity(compact, hay));
153
+ if (needlePy && needlePy !== compact && needlePy.length >= 2) {
154
+ best = Math.max(best, similarity(needlePy, hay));
155
+ }
156
+ });
157
+ const titlePy = entry.title_pinyin || "";
158
+ if (needlePy && titlePy && needlePy.length >= 2) {
159
+ best = Math.max(best, similarity(needlePy, titlePy));
160
+ if (titlePy.startsWith(needlePy) || needlePy.startsWith(titlePy)) {
161
+ best = Math.max(best, 0.82);
162
+ }
163
+ }
164
+ if (best < FUZZY_MIN_RATIO) return 0;
165
+ return Math.round(best * 100);
166
+ }
167
+
168
+ function matchScore(entry, query) {
169
+ const needle = lower(query).trim();
170
+ if (!needle) return 0;
171
+ const tokens = needle.split(/\s+/).filter(Boolean);
172
+ if (!tokens.length) return 0;
173
+ for (const token of tokens) {
174
+ if (!tokenMatches(entry, token)) {
175
+ const compactToken = token.replace(NOISE_RE, "");
176
+ if (!compactToken || !tokenMatches(entry, compactToken)) return 0;
177
+ }
178
+ }
179
+ const needleCompact = needle.replace(NOISE_RE, "");
180
+ const title = lower(entry.title);
181
+ const artist = lower(entry.artist);
182
+ const titleCompact = entry.title_compact || title.replace(NOISE_RE, "");
183
+ const artistCompact = entry.artist_compact || artist.replace(NOISE_RE, "");
184
+ let score = 10;
185
+ if (title === needle || titleCompact === needleCompact) score += 100;
186
+ else if (title.startsWith(needle) || titleCompact.startsWith(needleCompact)) score += 60;
187
+ else if (title.includes(needle) || titleCompact.includes(needleCompact)) score += 40;
188
+ if (artist === needle || artistCompact === needleCompact) score += 50;
189
+ else if (artist.includes(needle) || artistCompact.includes(needleCompact)) score += 25;
190
+ const titlePinyin = entry.title_pinyin || "";
191
+ const artistPinyin = entry.artist_pinyin || "";
192
+ const needlePy = queryPinyin(needle);
193
+ if (
194
+ needleCompact &&
195
+ (titlePinyin.startsWith(needleCompact) ||
196
+ artistPinyin.startsWith(needleCompact) ||
197
+ titlePinyin.includes(needleCompact) ||
198
+ artistPinyin.includes(needleCompact))
199
+ ) {
200
+ score += 20;
201
+ }
202
+ if (
203
+ needlePy &&
204
+ needlePy !== needleCompact &&
205
+ (titlePinyin.startsWith(needlePy) ||
206
+ titlePinyin.includes(needlePy) ||
207
+ artistPinyin.startsWith(needlePy) ||
208
+ artistPinyin.includes(needlePy))
209
+ ) {
210
+ score += 35;
211
+ }
212
+ const titleInitials = entry.title_initials || "";
213
+ const artistInitials = entry.artist_initials || "";
214
+ if (
215
+ needleCompact &&
216
+ (titleInitials.startsWith(needleCompact) ||
217
+ artistInitials.startsWith(needleCompact) ||
218
+ titleInitials.includes(needleCompact) ||
219
+ artistInitials.includes(needleCompact))
220
+ ) {
221
+ score += 15;
222
+ }
223
+ if (tokens.length === 1 && tokens[0].length === 1) {
224
+ const token = tokens[0];
225
+ if (token === entry.title_first_letter || token === entry.artist_first_letter) score += 12;
226
+ }
227
+ return score;
228
+ }
229
+
230
+ function bpmMatches(entry, bpmBucket, bpmExact) {
231
+ const raw = entry.bpm;
232
+ const bpm = Number(raw);
233
+ const hasBpm = raw !== "" && raw != null && Number.isFinite(bpm);
234
+ if (bpmExact || bpmBucket) {
235
+ if (!hasBpm) return false;
236
+ if (bpmExact) return Math.round(bpm) === parseInt(bpmExact, 10);
237
+ if (bpmBucket === "100-") return bpm < 100;
238
+ if (bpmBucket === "200+") return bpm >= 200;
239
+ const low = parseInt(bpmBucket, 10);
240
+ return bpm >= low && bpm < low + 10;
241
+ }
242
+ return true;
243
+ }
244
+
245
+ function letterMatches(entry, letter) {
246
+ const key = String(letter || "").trim();
247
+ if (!key) return true;
248
+ if (key === "#") return Boolean(entry.title_lead_non_zh_en);
249
+ return String(entry.title_first_letter || "").toLowerCase() === key.toLowerCase();
250
+ }
251
+
252
+ function titleSortKey(title) {
253
+ const text = String(title || "");
254
+ const ch = text.trim()[0] || "";
255
+ const nonZhEn = ch && !/[A-Za-z]/.test(ch) && !/[\u4e00-\u9fff]/.test(ch);
256
+ return [nonZhEn ? 0 : 1, text];
257
+ }
258
+
259
+ function searchCatalog(songs, query, filters) {
260
+ const list = Array.isArray(songs) ? songs : [];
261
+ const needle = String(query || "").trim();
262
+ const star = String((filters && filters.star) || "").trim();
263
+ const letter = String((filters && filters.letter) || "").trim();
264
+ const bpmBucket = String((filters && filters.bpmBucket) || "").trim();
265
+ const bpmExact = (filters && filters.bpmExact) || "";
266
+ const exact = [];
267
+ list.forEach((entry) => {
268
+ if (star && String(entry.star) !== star) return;
269
+ if (!letterMatches(entry, letter)) return;
270
+ if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
271
+ if (!needle) {
272
+ exact.push({ entry, score: 0, fuzzy: false });
273
+ return;
274
+ }
275
+ const score = matchScore(entry, needle);
276
+ if (score > 0) exact.push({ entry, score, fuzzy: false });
277
+ });
278
+ let ranked = exact;
279
+ if (needle && !ranked.length) {
280
+ const fuzzy = [];
281
+ list.forEach((entry) => {
282
+ if (star && String(entry.star) !== star) return;
283
+ if (!letterMatches(entry, letter)) return;
284
+ if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
285
+ const score = fuzzyScore(entry, needle);
286
+ if (score > 0) fuzzy.push({ entry, score, fuzzy: true });
287
+ });
288
+ fuzzy.sort((a, b) => b.score - a.score || titleSortKey(a.entry.title).join("").localeCompare(titleSortKey(b.entry.title).join("")));
289
+ ranked = fuzzy.slice(0, FUZZY_LIMIT);
290
+ } else if (needle) {
291
+ ranked.sort((a, b) => b.score - a.score || String(a.entry.title).localeCompare(String(b.entry.title), "zh"));
292
+ } else {
293
+ ranked.sort((a, b) => {
294
+ const ka = titleSortKey(a.entry.title);
295
+ const kb = titleSortKey(b.entry.title);
296
+ if (ka[0] !== kb[0]) return ka[0] - kb[0];
297
+ return String(ka[1]).localeCompare(String(kb[1]), "zh");
298
+ });
299
+ }
300
+ return ranked;
301
+ }
302
+
303
+ root.CatalogSearch = {
304
+ setCharPinyinMap,
305
+ queryPinyin,
306
+ matchScore,
307
+ fuzzyScore,
308
+ searchCatalog,
309
+ FUZZY_LIMIT,
310
+ };
311
+ })(typeof window !== "undefined" ? window : globalThis);