@delali/narsil 0.3.1-next.71 → 0.3.1-next.73

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.
Files changed (69) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/filesystem.mjs +1 -1
  3. package/dist/adapters/indexeddb.mjs +1 -1
  4. package/dist/{capabilities-Dt-7B4Wz.d.ts → capabilities-CYeAWYxQ.d.ts} +19 -4
  5. package/dist/{chunk-3JGWULDF.mjs → chunk-2WLD2NFW.mjs} +5 -5
  6. package/dist/{chunk-LMO5TJUS.mjs → chunk-3YKWSO54.mjs} +642 -196
  7. package/dist/{chunk-HWQLTLNG.mjs → chunk-4MQ5F4IL.mjs} +1 -1
  8. package/dist/{chunk-FVLQ63B7.mjs → chunk-AFKH4ZK3.mjs} +30 -6
  9. package/dist/{chunk-653BNMAF.mjs → chunk-AK3BLGCI.mjs} +195 -25
  10. package/dist/{chunk-IOR2K26O.mjs → chunk-CVFAFZSN.mjs} +8000 -4391
  11. package/dist/{chunk-USKFU7J5.mjs → chunk-I64LXKO3.mjs} +1 -1
  12. package/dist/{chunk-OI42GWLR.mjs → chunk-LJYTW7GT.mjs} +57 -11
  13. package/dist/{chunk-2STVUTNA.mjs → chunk-LPBWCHBE.mjs} +194 -13
  14. package/dist/{chunk-YNVIUGF5.mjs → chunk-PU43WDY4.mjs} +4 -2
  15. package/dist/{chunk-M76V3OGJ.mjs → chunk-THMSFOJO.mjs} +1289 -1405
  16. package/dist/{chunk-YJDQ7NLA.mjs → chunk-WVDFG45M.mjs} +93 -95
  17. package/dist/chunk-XW25X6AU.mjs +614 -0
  18. package/dist/chunk-ZKRV5M2P.mjs +75 -0
  19. package/dist/{chunk-P4DSQ6M4.mjs → chunk-ZULSEZCE.mjs} +68 -28
  20. package/dist/{client-COek5GXK.d.ts → client-D698E5ge.d.ts} +77 -26
  21. package/dist/client.d.ts +9 -9
  22. package/dist/client.mjs +45 -22
  23. package/dist/{config-rBpbgTw8.d.ts → config-Bv8etwUZ.d.ts} +11 -4
  24. package/dist/{config-Dfanyqqt.d.ts → config-QXU3HMpq.d.ts} +26 -2
  25. package/dist/direct-executor-5SRDFZAS.mjs +13 -0
  26. package/dist/distribution/coordinator/etcd.d.ts +29 -16
  27. package/dist/distribution/coordinator/etcd.mjs +163 -76
  28. package/dist/distribution/coordinator/in-memory.d.ts +2 -2
  29. package/dist/distribution/coordinator/in-memory.mjs +1 -1
  30. package/dist/distribution/transport/grpc.d.ts +2 -2
  31. package/dist/distribution/transport/grpc.mjs +22 -2
  32. package/dist/distribution/transport/in-memory.d.ts +2 -2
  33. package/dist/distribution/transport/in-memory.mjs +6 -3
  34. package/dist/distribution/transport/tcp.d.ts +2 -2
  35. package/dist/distribution/transport/tcp.mjs +40 -16
  36. package/dist/distribution.d.ts +9 -9
  37. package/dist/distribution.mjs +682 -388
  38. package/dist/embeddings/openai.mjs +1 -1
  39. package/dist/{engine-igk0aMpQ.d.ts → engine-D_OzKZ-6.d.ts} +97 -188
  40. package/dist/index.browser.mjs +11671 -7427
  41. package/dist/index.d.ts +27 -8
  42. package/dist/index.mjs +13 -14
  43. package/dist/invalidation/filesystem.mjs +1 -1
  44. package/dist/{memory-C7UMCMH3.d.ts → memory-B4jMmygw.d.ts} +279 -27
  45. package/dist/pattern-Cledccbl.d.ts +18 -0
  46. package/dist/pattern.d.ts +22 -0
  47. package/dist/pattern.mjs +7 -0
  48. package/dist/persistence/durability/checkpoint-worker.d.ts +41 -17
  49. package/dist/persistence/durability/checkpoint-worker.mjs +24 -16
  50. package/dist/react.d.ts +7 -7
  51. package/dist/react.mjs +1 -1
  52. package/dist/{schema-Bgzr4g7b.d.ts → schema-BBOCdhND.d.ts} +9 -7
  53. package/dist/server.d.ts +11 -11
  54. package/dist/server.mjs +213 -34
  55. package/dist/{tasks-BZt_zes6.d.ts → tasks-DpgCpW1x.d.ts} +3 -3
  56. package/dist/{thread-JEVAYETR.mjs → thread-3LNVA43V.mjs} +94 -27
  57. package/dist/{types-BQDlIvfS.d.ts → types-B3b68nfF.d.ts} +1 -1
  58. package/dist/{types-gOUqhs2F.d.ts → types-BiUmaiWT.d.ts} +1 -1
  59. package/dist/{types-D5OwhtsQ.d.ts → types-Cb9kiY5P.d.ts} +1 -1
  60. package/dist/vector/search-worker.d.ts +1 -1
  61. package/dist/vector/search-worker.mjs +4 -4
  62. package/dist/workers/entry.mjs +4 -4
  63. package/package.json +15 -7
  64. package/dist/chunk-3L4U6EJQ.mjs +0 -49
  65. package/dist/chunk-GIVAGD3Q.mjs +0 -25
  66. package/dist/chunk-IFBKLBGB.mjs +0 -7
  67. package/dist/direct-executor-GYJEKNV5.mjs +0 -12
  68. package/dist/serialization/crc32-worker.d.ts +0 -20
  69. package/dist/serialization/crc32-worker.mjs +0 -62
@@ -1,6 +1,7 @@
1
- import { isWordIndexedBase, flattenSchema, parsedTypeOf, isStringValuedBase, withSortableOption, isGeopointOrVectorType, MAX_SORT_FIELDS, MAX_SORT_FIELD_NAME_LENGTH, MAX_CURSOR_LENGTH, MAX_CURSOR_ANCHOR_CODE_POINTS, RESULT_WINDOW, DEFAULT_FUZZY_PREFIX_LENGTH, DEFAULT_PAGE_SIZE, PROTOTYPE_POLLUTION_KEYS, isWordIndexedFieldType, PREFIX_MAX_EXPANSIONS } from './chunk-653BNMAF.mjs';
2
- import { compareCodePoints, sortModeOf, defaultSortMode, SORT_VALUE_MAX_CODE_POINTS, toReducedSortValue, readSortField, compareComparableKeys, toComparableSortValue, exceedsCodePoints, isSortMode, SORT_MODES, compareSortValues } from './chunk-OI42GWLR.mjs';
3
- import { createNarsilError, ErrorCodes, NarsilError } from './chunk-USKFU7J5.mjs';
1
+ import { isWordIndexedBase, flattenSchema, MAX_LATITUDE_DEGREES, MAX_LONGITUDE_DEGREES, MIN_POLYGON_POINTS, parsedTypeOf, isStringValuedBase, withSortableOption, isGeopointOrVectorType, MAX_SORT_FIELDS, MAX_SORT_FIELD_NAME_LENGTH, MAX_CURSOR_LENGTH, MAX_CURSOR_ANCHOR_CODE_POINTS, RESULT_WINDOW, DEFAULT_FUZZY_PREFIX_LENGTH, EARTH_RADIUS_METERS, RADIUS_BOX_MARGIN_RATIO, RADIUS_BOX_MARGIN_RADIANS, FULL_TURN_DEGREES, ANTIMERIDIAN_SPAN_DEGREES, DEFAULT_PAGE_SIZE, PROTOTYPE_POLLUTION_KEYS, DEG_TO_RAD, LOCATION_CODE_BITS_PER_AXIS, isLatitude, isLongitude, isWordIndexedFieldType, PREFIX_MAX_EXPANSIONS, MAX_COVERING_CELLS } from './chunk-AK3BLGCI.mjs';
2
+ import { DEFAULT_PATTERN_WORK_CAP, PATTERN_TEST_OPERATORS, bitsetIsEmpty, MAX_PATTERN_TEXT_CODE_POINTS, MAX_PATTERN_POSITIONS, createBitSet, bitsetSet, PATTERN_WORK_FLUSH_UNITS, MATCHER_STATE_BUDGET_BYTES } from './chunk-ZKRV5M2P.mjs';
3
+ import { compareCodePoints, sortModeOf, defaultSortMode, SORT_VALUE_MAX_CODE_POINTS, toReducedSortValue, readSortField, compareComparableKeys, toComparableSortValue, exceedsCodePoints, holdsUnpairedSurrogate, isSortMode, SORT_MODES, compareSortValues, foldedCodePointCount } from './chunk-LJYTW7GT.mjs';
4
+ import { createNarsilError, ErrorCodes, NarsilError } from './chunk-I64LXKO3.mjs';
4
5
 
5
6
  // src/core/tokenizer/constants.ts
6
7
  var TOKEN_CACHE_SIZE_FLOOR = 5e4;
@@ -458,6 +459,100 @@ function* tokenizeIterator(text, language, options) {
458
459
  }
459
460
  }
460
461
 
462
+ // src/core/pattern-index/work-meter.ts
463
+ var COUNTER_BYTES = 8;
464
+ function capExceeded(cap) {
465
+ return new NarsilError(
466
+ ErrorCodes.SEARCH_WORK_CAP_EXCEEDED,
467
+ `The engine stopped this search before it checked every candidate, because the work on its pattern tests passed the cap of ${cap} units. Narrow the tests, or raise the patternWorkCap engine setting`,
468
+ { cap }
469
+ );
470
+ }
471
+ function localMeter(cap, spent, matcherStateBytes, flushUnits) {
472
+ let total = spent;
473
+ let sharedCount = null;
474
+ const spentSoFar = () => sharedCount === null ? total : Number(Atomics.load(sharedCount, 0));
475
+ const meter = {
476
+ cap,
477
+ matcherStateBytes,
478
+ flushUnits,
479
+ add(units) {
480
+ if (units <= 0) return;
481
+ if (sharedCount === null) total += units;
482
+ else total = Number(Atomics.add(sharedCount, 0, BigInt(units))) + units;
483
+ if (total > cap) throw capExceeded(cap);
484
+ },
485
+ remaining() {
486
+ return cap - spentSoFar();
487
+ },
488
+ fork(workerCount) {
489
+ const copies = Math.max(1, workerCount);
490
+ const seed = spentSoFar();
491
+ let counter = null;
492
+ if (typeof SharedArrayBuffer === "function") {
493
+ counter = new SharedArrayBuffer(COUNTER_BYTES);
494
+ new BigInt64Array(counter)[0] = BigInt(seed);
495
+ }
496
+ const shared = {
497
+ cap,
498
+ spent: seed,
499
+ counter,
500
+ matcherStateBytes: Math.floor(matcherStateBytes / copies),
501
+ flushUnits: Math.max(1, Math.floor(flushUnits / copies))
502
+ };
503
+ return {
504
+ shared,
505
+ settle(reportedUnits) {
506
+ if (counter === null) {
507
+ meter.add(reportedUnits);
508
+ return;
509
+ }
510
+ const unitsSinceFork = spentSoFar() - seed;
511
+ const view = new BigInt64Array(counter);
512
+ if (unitsSinceFork > 0) Atomics.add(view, 0, BigInt(unitsSinceFork));
513
+ sharedCount = view;
514
+ total = spentSoFar();
515
+ if (total > cap) throw capExceeded(cap);
516
+ }
517
+ };
518
+ }
519
+ };
520
+ return meter;
521
+ }
522
+ function createPatternWorkMeter(cap = DEFAULT_PATTERN_WORK_CAP) {
523
+ return localMeter(cap, 0, MATCHER_STATE_BUDGET_BYTES, PATTERN_WORK_FLUSH_UNITS);
524
+ }
525
+ function sharedMeter(work, counter) {
526
+ const view = new BigInt64Array(counter);
527
+ return {
528
+ matcherStateBytes: work.matcherStateBytes,
529
+ flushUnits: work.flushUnits,
530
+ add(units) {
531
+ if (units <= 0) return;
532
+ const total = Number(Atomics.add(view, 0, BigInt(units))) + units;
533
+ if (total > work.cap) throw capExceeded(work.cap);
534
+ },
535
+ remaining() {
536
+ return work.cap - Number(Atomics.load(view, 0));
537
+ }
538
+ };
539
+ }
540
+ function joinPatternWorkMeter(work) {
541
+ const meter = work.counter === null ? localMeter(work.cap, work.spent, work.matcherStateBytes, work.flushUnits) : sharedMeter(work, work.counter);
542
+ let spent = 0;
543
+ return {
544
+ matcherStateBytes: meter.matcherStateBytes,
545
+ flushUnits: meter.flushUnits,
546
+ add(units) {
547
+ if (units <= 0) return;
548
+ spent += units;
549
+ meter.add(units);
550
+ },
551
+ remaining: () => meter.remaining(),
552
+ spent: () => spent
553
+ };
554
+ }
555
+
461
556
  // src/partitioning/distributed-scoring.ts
462
557
  function analysedQueryTokens(term, language, analysis) {
463
558
  const { tokens } = tokenize(term, language, {
@@ -673,67 +768,6 @@ function heapPop(heap) {
673
768
  return top;
674
769
  }
675
770
 
676
- // src/core/bitset.ts
677
- function createBitSet(capacity) {
678
- return new Uint32Array(capacity + 31 >>> 5 || 1);
679
- }
680
- function bitsetSet(bs, index) {
681
- const word = index >>> 5;
682
- if (word < bs.length) {
683
- bs[word] |= 1 << (index & 31);
684
- }
685
- }
686
- function bitsetHas(bs, index) {
687
- const word = index >>> 5;
688
- if (word >= bs.length) return false;
689
- return (bs[word] & 1 << (index & 31)) !== 0;
690
- }
691
- function bitsetAnd(a, b) {
692
- const len = Math.min(a.length, b.length);
693
- const result = new Uint32Array(len);
694
- for (let i = 0; i < len; i++) {
695
- result[i] = a[i] & b[i];
696
- }
697
- return result;
698
- }
699
- function bitsetOr(a, b) {
700
- const longer = a.length >= b.length ? a : b;
701
- const shorter = a.length >= b.length ? b : a;
702
- const result = new Uint32Array(longer.length);
703
- for (let i = 0; i < shorter.length; i++) {
704
- result[i] = longer[i] | shorter[i];
705
- }
706
- for (let i = shorter.length; i < longer.length; i++) {
707
- result[i] = longer[i];
708
- }
709
- return result;
710
- }
711
- function bitsetNot(bs, capacity) {
712
- const result = new Uint32Array(bs.length);
713
- const fullWords = capacity >>> 5;
714
- for (let i = 0; i < fullWords; i++) {
715
- result[i] = ~bs[i];
716
- }
717
- const remainder = capacity & 31;
718
- if (remainder > 0 && fullWords < bs.length) {
719
- result[fullWords] = ~bs[fullWords] & (1 << remainder) - 1;
720
- }
721
- return result;
722
- }
723
- function bitsetIsEmpty(bs) {
724
- for (let i = 0; i < bs.length; i++) {
725
- if (bs[i] !== 0) return false;
726
- }
727
- return true;
728
- }
729
- function bitsetFromSet(set, capacity) {
730
- const bs = createBitSet(capacity);
731
- for (const id of set) {
732
- bs[id >>> 5] |= 1 << (id & 31);
733
- }
734
- return bs;
735
- }
736
-
737
771
  // src/core/partition/utils.ts
738
772
  function getOrCreateFieldNameIndex(table, fieldName) {
739
773
  const existing = table.indexMap.get(fieldName);
@@ -743,6 +777,7 @@ function getOrCreateFieldNameIndex(table, fieldName) {
743
777
  table.indexMap.set(fieldName, idx);
744
778
  return idx;
745
779
  }
780
+ var VECTOR_PATTERN = /^vector\[(\d+)]$/;
746
781
  function getNestedValue(obj, path) {
747
782
  const parts = path.split(".");
748
783
  let current = obj;
@@ -775,6 +810,10 @@ function textFieldsChanged(oldDoc, newDoc, fields) {
775
810
  }
776
811
  return false;
777
812
  }
813
+ function parseVectorDimension(fieldType) {
814
+ const match = VECTOR_PATTERN.exec(fieldType);
815
+ return match ? Number.parseInt(match[1], 10) : null;
816
+ }
778
817
  function tokenizeOptions(options) {
779
818
  return {
780
819
  stem: true,
@@ -800,13 +839,6 @@ function getFieldValueByInternalId(docStore, internalId, fieldPath) {
800
839
  if (externalId === void 0) return void 0;
801
840
  return getFieldValueForDoc(docStore, externalId, fieldPath);
802
841
  }
803
- function getAllInternalDocIds(docStore) {
804
- const ids = /* @__PURE__ */ new Set();
805
- for (const internalId of docStore.allInternalIds()) {
806
- ids.add(internalId);
807
- }
808
- return ids;
809
- }
810
842
 
811
843
  // src/core/partition/facets.ts
812
844
  var UNDECLARED_FIELD_TYPE = "";
@@ -1084,13 +1116,205 @@ function decodeCursorText(encoded) {
1084
1116
  return new TextDecoder().decode(bytes);
1085
1117
  }
1086
1118
 
1087
- // src/geo/constants.ts
1088
- var DEG_TO_RAD = Math.PI / 180;
1089
- var MIN_POLYGON_POINTS = 3;
1090
- var ANTIMERIDIAN_SPAN_DEGREES = 180;
1091
- var FULL_TURN_DEGREES = 360;
1092
- var VINCENTY_MAX_ITERATIONS = 200;
1093
- var VINCENTY_CONVERGENCE_THRESHOLD = 1e-12;
1119
+ // src/filters/constants.ts
1120
+ var MAX_FILTER_DEPTH = 30;
1121
+ var MAX_FILTER_TESTS = 1024;
1122
+ var MAX_FILTER_ARRAY_SIZE = 65536;
1123
+ var GEO_UNIT_METRES = { km: 1e3, mi: 1609.344, m: 1 };
1124
+
1125
+ // src/filters/operands/checks.ts
1126
+ function operandKindOf(type) {
1127
+ if (type === void 0) return void 0;
1128
+ if (type === "number" || type === "number[]") return "number";
1129
+ if (type === "boolean" || type === "boolean[]") return "boolean";
1130
+ if (type === "geopoint") return "geopoint";
1131
+ if (type.startsWith("vector[")) return "vector";
1132
+ return "text";
1133
+ }
1134
+ function isFiniteNumber(value) {
1135
+ return typeof value === "number" && Number.isFinite(value);
1136
+ }
1137
+ function kindOfValue(value) {
1138
+ if (isFiniteNumber(value)) return "number";
1139
+ if (typeof value === "boolean") return "boolean";
1140
+ if (typeof value === "string") return "text";
1141
+ return void 0;
1142
+ }
1143
+ function isObjectRecord(value) {
1144
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1145
+ }
1146
+ function invalid(message, fieldPath, operator) {
1147
+ throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
1148
+ }
1149
+ function requireFieldKind(fieldPath, operator, type, accepted) {
1150
+ const kind = operandKindOf(type);
1151
+ if (kind === void 0 || accepted.includes(kind)) return;
1152
+ invalid(
1153
+ `The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
1154
+ fieldPath,
1155
+ operator
1156
+ );
1157
+ }
1158
+ function requireTextRangeField(fieldPath, operator, type) {
1159
+ const parsed = parsedTypeOf(type);
1160
+ if (parsed?.sortable && (parsed.base === "string" || parsed.base === "verbatim")) return;
1161
+ const declared = type === void 0 ? `the schema has no field "${fieldPath}"` : `field "${fieldPath}" has type "${type}"`;
1162
+ invalid(
1163
+ `The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
1164
+ fieldPath,
1165
+ operator
1166
+ );
1167
+ }
1168
+ function requireListLength(fieldPath, operator, values) {
1169
+ if (values.length <= MAX_FILTER_ARRAY_SIZE) return;
1170
+ invalid(
1171
+ `"${operator}" on field "${fieldPath}" holds ${values.length} values, while a filter list holds at most ${MAX_FILTER_ARRAY_SIZE}`,
1172
+ fieldPath,
1173
+ operator
1174
+ );
1175
+ }
1176
+
1177
+ // src/geo/cells.ts
1178
+ var AXIS_CELLS = 2 ** LOCATION_CODE_BITS_PER_AXIS;
1179
+ function axisCell(value, lowest, span) {
1180
+ const cell = Math.floor((value - lowest) / span * AXIS_CELLS);
1181
+ if (!(cell > 0)) return 0;
1182
+ return cell >= AXIS_CELLS ? AXIS_CELLS - 1 : cell;
1183
+ }
1184
+ function latitudeCell(lat) {
1185
+ return axisCell(lat, -MAX_LATITUDE_DEGREES, 2 * MAX_LATITUDE_DEGREES);
1186
+ }
1187
+ function longitudeCell(lon) {
1188
+ return axisCell(lon, -MAX_LONGITUDE_DEGREES, FULL_TURN_DEGREES);
1189
+ }
1190
+ function interleave(row, column, bits) {
1191
+ let code = 0;
1192
+ for (let bit = bits - 1; bit >= 0; bit--) code = code * 4 + (row >>> bit & 1) * 2 + (column >>> bit & 1);
1193
+ return code;
1194
+ }
1195
+ function locationCode(lat, lon) {
1196
+ return interleave(latitudeCell(lat), longitudeCell(lon), LOCATION_CODE_BITS_PER_AXIS);
1197
+ }
1198
+ function appendBoxRanges(box, out) {
1199
+ const south = latitudeCell(box.south);
1200
+ const north = latitudeCell(box.north);
1201
+ const west = longitudeCell(box.west);
1202
+ const east = longitudeCell(box.east);
1203
+ let level = LOCATION_CODE_BITS_PER_AXIS;
1204
+ while (level > 0) {
1205
+ const shift2 = LOCATION_CODE_BITS_PER_AXIS - level;
1206
+ const rows = (north >>> shift2) - (south >>> shift2) + 1;
1207
+ const columns = (east >>> shift2) - (west >>> shift2) + 1;
1208
+ if (rows * columns <= MAX_COVERING_CELLS) break;
1209
+ level--;
1210
+ }
1211
+ const shift = LOCATION_CODE_BITS_PER_AXIS - level;
1212
+ const codesPerCell = 4 ** shift;
1213
+ for (let row = south >>> shift; row <= north >>> shift; row++) {
1214
+ for (let column = west >>> shift; column <= east >>> shift; column++) {
1215
+ const from = interleave(row, column, level) * codesPerCell;
1216
+ out.push({ from, to: from + codesPerCell });
1217
+ }
1218
+ }
1219
+ }
1220
+ function coveringRanges(boxes) {
1221
+ const ranges = [];
1222
+ for (const box of boxes) appendBoxRanges(box, ranges);
1223
+ ranges.sort((left, right) => left.from - right.from);
1224
+ const merged = [];
1225
+ for (const range of ranges) {
1226
+ const last = merged[merged.length - 1];
1227
+ if (last !== void 0 && range.from <= last.to) {
1228
+ if (range.to > last.to) last.to = range.to;
1229
+ } else {
1230
+ merged.push({ from: range.from, to: range.to });
1231
+ }
1232
+ }
1233
+ return merged;
1234
+ }
1235
+ function longitudeBoxes(south, north, west, east) {
1236
+ if (east - west >= FULL_TURN_DEGREES) {
1237
+ return [{ south, north, west: -MAX_LONGITUDE_DEGREES, east: MAX_LONGITUDE_DEGREES }];
1238
+ }
1239
+ if (west < -MAX_LONGITUDE_DEGREES) {
1240
+ return [
1241
+ { south, north, west: west + FULL_TURN_DEGREES, east: MAX_LONGITUDE_DEGREES },
1242
+ { south, north, west: -MAX_LONGITUDE_DEGREES, east }
1243
+ ];
1244
+ }
1245
+ if (east > MAX_LONGITUDE_DEGREES) {
1246
+ return [
1247
+ { south, north, west, east: MAX_LONGITUDE_DEGREES },
1248
+ { south, north, west: -MAX_LONGITUDE_DEGREES, east: east - FULL_TURN_DEGREES }
1249
+ ];
1250
+ }
1251
+ return [{ south, north, west, east }];
1252
+ }
1253
+ function radiusBoxes(lat, lon, distanceMeters) {
1254
+ const angle = distanceMeters / EARTH_RADIUS_METERS * RADIUS_BOX_MARGIN_RATIO + RADIUS_BOX_MARGIN_RADIANS;
1255
+ const angleDegrees = angle / DEG_TO_RAD;
1256
+ const south = lat - angleDegrees;
1257
+ const north = lat + angleDegrees;
1258
+ if (angle >= Math.PI || south <= -MAX_LATITUDE_DEGREES || north >= MAX_LATITUDE_DEGREES) {
1259
+ return [
1260
+ {
1261
+ south: Math.max(south, -MAX_LATITUDE_DEGREES),
1262
+ north: Math.min(north, MAX_LATITUDE_DEGREES),
1263
+ west: -MAX_LONGITUDE_DEGREES,
1264
+ east: MAX_LONGITUDE_DEGREES
1265
+ }
1266
+ ];
1267
+ }
1268
+ const spread = Math.asin(Math.sin(angle) / Math.cos(lat * DEG_TO_RAD)) / DEG_TO_RAD;
1269
+ return longitudeBoxes(south, north, lon - spread, lon + spread);
1270
+ }
1271
+
1272
+ // src/geo/edge-tree.ts
1273
+ function buildLongitudeEdgeTree(lons) {
1274
+ const n = lons.length;
1275
+ const edges = [];
1276
+ for (let i = 0; i < n; i++) {
1277
+ if (lons[i] !== lons[i === 0 ? n - 1 : i - 1]) edges.push(i);
1278
+ }
1279
+ const lowOf = (i) => Math.min(lons[i], lons[i === 0 ? n - 1 : i - 1]);
1280
+ edges.sort((left, right) => lowOf(left) - lowOf(right));
1281
+ const count = edges.length;
1282
+ const tree = {
1283
+ from: new Int32Array(count),
1284
+ to: new Int32Array(count),
1285
+ low: new Float64Array(count),
1286
+ high: new Float64Array(count),
1287
+ subtreeHigh: new Float64Array(count)
1288
+ };
1289
+ edges.forEach((i, at) => {
1290
+ const j = i === 0 ? n - 1 : i - 1;
1291
+ tree.from[at] = i;
1292
+ tree.to[at] = j;
1293
+ tree.low[at] = Math.min(lons[i], lons[j]);
1294
+ tree.high[at] = Math.max(lons[i], lons[j]);
1295
+ });
1296
+ const fillSubtreeHigh = (start, end) => {
1297
+ if (start >= end) return Number.NEGATIVE_INFINITY;
1298
+ const mid = start + end >>> 1;
1299
+ const highest = Math.max(tree.high[mid], fillSubtreeHigh(start, mid), fillSubtreeHigh(mid + 1, end));
1300
+ tree.subtreeHigh[mid] = highest;
1301
+ return highest;
1302
+ };
1303
+ fillSubtreeHigh(0, count);
1304
+ return tree;
1305
+ }
1306
+ function forEachEdgeSpanning(tree, lon, visit) {
1307
+ const walk = (start, end) => {
1308
+ if (start >= end) return;
1309
+ const mid = start + end >>> 1;
1310
+ if (tree.subtreeHigh[mid] <= lon) return;
1311
+ walk(start, mid);
1312
+ if (tree.low[mid] > lon) return;
1313
+ if (lon < tree.high[mid]) visit(tree.from[mid], tree.to[mid]);
1314
+ walk(mid + 1, end);
1315
+ };
1316
+ walk(0, tree.from.length);
1317
+ }
1094
1318
 
1095
1319
  // src/geo/polygon.ts
1096
1320
  function requirePolygonRing(points) {
@@ -1103,11 +1327,18 @@ function requirePolygonRing(points) {
1103
1327
  }
1104
1328
  points.forEach((point, position) => {
1105
1329
  const { lat, lon } = point ?? {};
1106
- if (typeof lat === "number" && Number.isFinite(lat) && typeof lon === "number" && Number.isFinite(lon)) return;
1330
+ if (typeof lat !== "number" || !Number.isFinite(lat) || typeof lon !== "number" || !Number.isFinite(lon)) {
1331
+ throw new NarsilError(
1332
+ ErrorCodes.SEARCH_INVALID_FILTER,
1333
+ `A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
1334
+ { position }
1335
+ );
1336
+ }
1337
+ if (isLatitude(lat) && isLongitude(lon)) return;
1107
1338
  throw new NarsilError(
1108
1339
  ErrorCodes.SEARCH_INVALID_FILTER,
1109
- `A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
1110
- { position }
1340
+ `A polygon point must hold a "lat" from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES} and a "lon" from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, while the point at position ${position} holds ${lat}, ${lon}`,
1341
+ { position, lat, lon }
1111
1342
  );
1112
1343
  });
1113
1344
  }
@@ -1140,67 +1371,40 @@ function polygonContainment(polygon) {
1140
1371
  lats[i] = polygon[i].lat;
1141
1372
  lons[i] = unwrap ? eastward(polygon[i].lon) : polygon[i].lon;
1142
1373
  }
1374
+ const edges = buildLongitudeEdgeTree(lons);
1143
1375
  return (lat, rawLon) => {
1144
1376
  const lon = unwrap ? eastward(rawLon) : rawLon;
1145
1377
  let inside = false;
1146
- for (let i = 0, j = n - 1; i < n; j = i++) {
1147
- const lonI = lons[i];
1148
- const lonJ = lons[j];
1149
- if (lonI > lon !== lonJ > lon) {
1150
- const intersectLat = lats[i] + (lats[j] - lats[i]) / (lonJ - lonI) * (lon - lonI);
1151
- if (lat < intersectLat) inside = !inside;
1152
- }
1153
- }
1378
+ forEachEdgeSpanning(edges, lon, (i, j) => {
1379
+ const intersectLat = lats[i] + (lats[j] - lats[i]) / (lons[j] - lons[i]) * (lon - lons[i]);
1380
+ if (lat < intersectLat) inside = !inside;
1381
+ });
1154
1382
  return inside;
1155
1383
  };
1156
1384
  }
1157
-
1158
- // src/filters/constants.ts
1159
- var MAX_FILTER_DEPTH = 30;
1160
-
1161
- // src/filters/operands.ts
1162
- var RANGE_OPERATORS = ["gt", "lt", "gte", "lte"];
1163
- var TEXT_OPERATORS = ["startsWith", "endsWith"];
1164
- var EQUALITY_OPERATORS = ["eq", "ne"];
1165
- var LIST_OPERATORS = ["in", "nin"];
1166
- function operandKindOf(type) {
1167
- if (type === void 0) return void 0;
1168
- if (type === "number" || type === "number[]") return "number";
1169
- if (type === "boolean" || type === "boolean[]") return "boolean";
1170
- if (type === "geopoint") return "geopoint";
1171
- if (type.startsWith("vector[")) return "vector";
1172
- return "text";
1173
- }
1174
- function isFiniteNumber(value) {
1175
- return typeof value === "number" && Number.isFinite(value);
1176
- }
1177
- function kindOfValue(value) {
1178
- if (isFiniteNumber(value)) return "number";
1179
- if (typeof value === "boolean") return "boolean";
1180
- if (typeof value === "string") return "text";
1181
- return void 0;
1182
- }
1183
- function invalid(message, fieldPath, operator) {
1184
- throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
1185
- }
1186
- function requireFieldKind(fieldPath, operator, type, accepted) {
1187
- const kind = operandKindOf(type);
1188
- if (kind === void 0 || accepted.includes(kind)) return;
1189
- invalid(
1190
- `The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
1191
- fieldPath,
1192
- operator
1193
- );
1385
+ function polygonBoxes(polygon) {
1386
+ if (polygon.length < MIN_POLYGON_POINTS) return [];
1387
+ const unwrap = crossesAntimeridian(polygon);
1388
+ let south = Number.POSITIVE_INFINITY;
1389
+ let north = Number.NEGATIVE_INFINITY;
1390
+ let west = Number.POSITIVE_INFINITY;
1391
+ let east = Number.NEGATIVE_INFINITY;
1392
+ for (const point of polygon) {
1393
+ const lon = unwrap ? eastward(point.lon) : point.lon;
1394
+ south = Math.min(south, point.lat);
1395
+ north = Math.max(north, point.lat);
1396
+ west = Math.min(west, lon);
1397
+ east = Math.max(east, lon);
1398
+ }
1399
+ if (west >= MAX_LONGITUDE_DEGREES)
1400
+ return longitudeBoxes(south, north, west - FULL_TURN_DEGREES, east - FULL_TURN_DEGREES);
1401
+ return longitudeBoxes(south, north, west, east);
1194
1402
  }
1195
- function requireTextRangeField(fieldPath, operator, type) {
1196
- const parsed = parsedTypeOf(type);
1197
- if (parsed?.sortable && (parsed.base === "string" || parsed.base === "verbatim")) return;
1198
- const declared = type === void 0 ? `the schema has no field "${fieldPath}"` : `field "${fieldPath}" has type "${type}"`;
1199
- invalid(
1200
- `The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
1201
- fieldPath,
1202
- operator
1203
- );
1403
+
1404
+ // src/filters/operands/geo.ts
1405
+ function requireFlag(fieldPath, operator, flag, value) {
1406
+ if (value === void 0 || typeof value === "boolean") return;
1407
+ invalid(`"${operator}.${flag}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
1204
1408
  }
1205
1409
  function requireRadius(fieldPath, radius) {
1206
1410
  const circle = radius;
@@ -1212,6 +1416,15 @@ function requireRadius(fieldPath, radius) {
1212
1416
  invalid(`"radius.${key}" on field "${fieldPath}" must be a finite number`, fieldPath, "radius");
1213
1417
  }
1214
1418
  }
1419
+ if (typeof circle.unit !== "string" || !Object.hasOwn(GEO_UNIT_METRES, circle.unit)) {
1420
+ invalid(
1421
+ `"radius.unit" on field "${fieldPath}" must be one of ${Object.keys(GEO_UNIT_METRES).join(", ")}, and this filter sets ${String(circle.unit)}`,
1422
+ fieldPath,
1423
+ "radius"
1424
+ );
1425
+ }
1426
+ requireFlag(fieldPath, "radius", "inside", circle.inside);
1427
+ requireFlag(fieldPath, "radius", "highPrecision", circle.highPrecision);
1215
1428
  if (circle.distance < 0) {
1216
1429
  invalid(
1217
1430
  `"radius.distance" on field "${fieldPath}" must be zero or more, and this filter sets ${String(circle.distance)}`,
@@ -1219,13 +1432,197 @@ function requireRadius(fieldPath, radius) {
1219
1432
  "radius"
1220
1433
  );
1221
1434
  }
1435
+ if (!isLatitude(circle.lat)) {
1436
+ invalid(
1437
+ `"radius.lat" on field "${fieldPath}" must be from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES}, and this filter sets ${String(circle.lat)}`,
1438
+ fieldPath,
1439
+ "radius"
1440
+ );
1441
+ }
1442
+ if (!isLongitude(circle.lon)) {
1443
+ invalid(
1444
+ `"radius.lon" on field "${fieldPath}" must be from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, and this filter sets ${String(circle.lon)}`,
1445
+ fieldPath,
1446
+ "radius"
1447
+ );
1448
+ }
1449
+ }
1450
+ function requireGeoOperands(fieldPath, filter) {
1451
+ if (filter.radius !== void 0) requireRadius(fieldPath, filter.radius);
1452
+ if (filter.polygon === void 0) return;
1453
+ if (!isObjectRecord(filter.polygon)) {
1454
+ invalid(`"polygon" on field "${fieldPath}" must be an object`, fieldPath, "polygon");
1455
+ }
1456
+ requireFlag(fieldPath, "polygon", "inside", filter.polygon.inside);
1457
+ const points = filter.polygon.points;
1458
+ if (Array.isArray(points)) requireListLength(fieldPath, "polygon", points);
1459
+ requirePolygonRing(points);
1460
+ }
1461
+
1462
+ // src/filters/operands/lists.ts
1463
+ var WHOLE_VALUE_OPERATORS = ["eq", "ne", "in", "nin", "startsWith", "endsWith", "matchesAny", "containsAll"];
1464
+ var ELEMENT_OPERATORS = ["matchesAny", "containsAll"];
1465
+ var PRESENCE_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
1466
+ var SIZE_OPERATORS = ["eq", "ne", "gt", "gte", "lt", "lte", "between"];
1467
+ var SIZE_OPERATOR_SET = new Set(SIZE_OPERATORS);
1468
+ var ELEMENT_KIND_BY_BASE = {
1469
+ "number[]": "number",
1470
+ "boolean[]": "boolean",
1471
+ "enum[]": "text",
1472
+ "string[]": "text",
1473
+ "verbatim[]": "text"
1474
+ };
1475
+ function requireWholeValueIndex(fieldPath, filter, type) {
1476
+ const parsed = parsedTypeOf(type);
1477
+ if (parsed === void 0 || parsed.pattern || parsed.base !== "string" && parsed.base !== "string[]") return;
1478
+ for (const operator of WHOLE_VALUE_OPERATORS) {
1479
+ if (filter[operator] === void 0) continue;
1480
+ invalid(
1481
+ `"${operator}" compares whole values of field "${fieldPath}", which the engine finds only through a pattern index. A "${parsed.base}" field keeps a pattern index where its type includes "pattern", so declare the field as "${parsed.base}:pattern" in a new index to filter it this way`,
1482
+ fieldPath,
1483
+ operator
1484
+ );
1485
+ }
1486
+ }
1487
+ function requirePresenceOperands(fieldPath, filter) {
1488
+ for (const operator of PRESENCE_OPERATORS) {
1489
+ const operand = filter[operator];
1490
+ if (operand !== void 0 && typeof operand !== "boolean") {
1491
+ invalid(`"${operator}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
1492
+ }
1493
+ }
1494
+ }
1495
+ function listElementKind(fieldPath, operator, type) {
1496
+ const base = parsedTypeOf(type)?.base;
1497
+ const kind = base === void 0 ? void 0 : ELEMENT_KIND_BY_BASE[base];
1498
+ if (kind !== void 0) return kind;
1499
+ return invalid(
1500
+ `"${operator}" tests the elements of a list field, while the schema declares field "${fieldPath}" as "${type}"`,
1501
+ fieldPath,
1502
+ operator
1503
+ );
1504
+ }
1505
+ function requireElementOperands(fieldPath, filter, type) {
1506
+ for (const operator of ELEMENT_OPERATORS) {
1507
+ const values = filter[operator];
1508
+ if (values === void 0) continue;
1509
+ if (!Array.isArray(values)) invalid(`"${operator}" on field "${fieldPath}" must be a list`, fieldPath, operator);
1510
+ requireListLength(fieldPath, operator, values);
1511
+ const kind = listElementKind(fieldPath, operator, type);
1512
+ if (values.some((value) => kindOfValue(value) !== kind)) {
1513
+ invalid(
1514
+ `Every value of "${operator}" on field "${fieldPath}" must be a ${kind === "text" ? "string" : kind === "number" ? "finite number" : "boolean"}, because the field holds a list of them`,
1515
+ fieldPath,
1516
+ operator
1517
+ );
1518
+ }
1519
+ }
1520
+ }
1521
+ function requireSizeOperands(fieldPath, filter, type) {
1522
+ const size = filter.size;
1523
+ if (size === void 0) return;
1524
+ if (!isObjectRecord(size)) invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
1525
+ listElementKind(fieldPath, "size", type);
1526
+ for (const [operator, operand] of Object.entries(size)) {
1527
+ if (!SIZE_OPERATOR_SET.has(operator)) {
1528
+ invalid(
1529
+ `"size" on field "${fieldPath}" takes ${SIZE_OPERATORS.join(", ")}, while "${operator}" is none of them`,
1530
+ fieldPath,
1531
+ "size"
1532
+ );
1533
+ }
1534
+ const bounds = operator === "between" ? operand : [operand];
1535
+ if (!Array.isArray(bounds) || bounds.length !== (operator === "between" ? 2 : 1) || !bounds.every(isFiniteNumber)) {
1536
+ invalid(
1537
+ `"size.${operator}" on field "${fieldPath}" must be ${operator === "between" ? "two finite numbers, the lower bound first" : "a finite number"}`,
1538
+ fieldPath,
1539
+ "size"
1540
+ );
1541
+ }
1542
+ }
1222
1543
  }
1544
+
1545
+ // src/filters/operands/pattern.ts
1546
+ function requirePatternField(fieldPath, operator, type) {
1547
+ if (parsedTypeOf(type)?.pattern === true) return;
1548
+ const declared = type === void 0 ? `The schema declares no field "${fieldPath}"` : `Field "${fieldPath}" has type "${type}"`;
1549
+ invalid(
1550
+ `"${operator}" applies only to a pattern field, because only a pattern field has a pattern index. A pattern field is a "verbatim" field, or a text field whose type includes "pattern". ${declared}`,
1551
+ fieldPath,
1552
+ operator
1553
+ );
1554
+ }
1555
+ function requirePatternText(fieldPath, operator, text, caseFold) {
1556
+ if (exceedsCodePoints(text, MAX_PATTERN_TEXT_CODE_POINTS)) {
1557
+ invalid(
1558
+ `The text of "${operator}" on pattern field "${fieldPath}" holds more than ${MAX_PATTERN_TEXT_CODE_POINTS} code points`,
1559
+ fieldPath,
1560
+ operator
1561
+ );
1562
+ }
1563
+ if (caseFold && foldedCodePointCount(text) > MAX_PATTERN_POSITIONS) {
1564
+ invalid(
1565
+ `The text of "${operator}" on pattern field "${fieldPath}" has a case fold of more than ${MAX_PATTERN_POSITIONS} code points, while a pattern holds at most ${MAX_PATTERN_POSITIONS} positions`,
1566
+ fieldPath,
1567
+ operator
1568
+ );
1569
+ }
1570
+ }
1571
+ var ELEMENT_OPERATORS2 = ["matchesAny", "containsAll"];
1572
+ function requirePatternOperands(fieldPath, filter, type) {
1573
+ if (filter.contains !== void 0) {
1574
+ if (typeof filter.contains !== "string") {
1575
+ invalid(`"contains" on field "${fieldPath}" must be a string`, fieldPath, "contains");
1576
+ }
1577
+ requirePatternField(fieldPath, "contains", type);
1578
+ }
1579
+ if (filter.caseFold !== void 0) {
1580
+ if (typeof filter.caseFold !== "boolean") {
1581
+ invalid(`"caseFold" on field "${fieldPath}" must be true or false`, fieldPath, "caseFold");
1582
+ }
1583
+ requirePatternField(fieldPath, "caseFold", type);
1584
+ }
1585
+ if (parsedTypeOf(type)?.pattern !== true) return;
1586
+ const caseFold = filter.caseFold === true;
1587
+ for (const operator of PATTERN_TEST_OPERATORS) {
1588
+ const operand = filter[operator];
1589
+ if (typeof operand === "string") requirePatternText(fieldPath, operator, operand, caseFold);
1590
+ if (!Array.isArray(operand)) continue;
1591
+ for (const text of operand) {
1592
+ if (typeof text === "string") requirePatternText(fieldPath, operator, text, caseFold);
1593
+ }
1594
+ }
1595
+ for (const operator of ELEMENT_OPERATORS2) {
1596
+ const operand = filter[operator];
1597
+ if (!Array.isArray(operand)) continue;
1598
+ for (const text of operand) {
1599
+ if (typeof text === "string") requirePatternText(fieldPath, operator, text, false);
1600
+ }
1601
+ }
1602
+ }
1603
+ function requireWellFormedText(fieldPath, filter) {
1604
+ for (const [operator, operand] of Object.entries(filter)) {
1605
+ const texts = Array.isArray(operand) ? operand : [operand];
1606
+ for (const text of texts) {
1607
+ if (typeof text !== "string" || !holdsUnpairedSurrogate(text)) continue;
1608
+ invalid(
1609
+ `The text of "${operator}" on field "${fieldPath}" holds an unpaired surrogate, the half of a character such as an emoji whose other half is missing, so no stored value can hold it`,
1610
+ fieldPath,
1611
+ operator
1612
+ );
1613
+ }
1614
+ }
1615
+ }
1616
+
1617
+ // src/filters/operands/index.ts
1618
+ var RANGE_OPERATORS = ["gt", "lt", "gte", "lte"];
1619
+ var TEXT_OPERATORS = ["startsWith", "endsWith"];
1620
+ var EQUALITY_OPERATORS = ["eq", "ne"];
1621
+ var LIST_OPERATORS = ["in", "nin"];
1622
+ var GEO_OPERATORS = ["radius", "polygon"];
1223
1623
  function invalidShape(message) {
1224
1624
  throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message);
1225
1625
  }
1226
- function isObjectRecord(value) {
1227
- return value !== null && typeof value === "object" && !Array.isArray(value);
1228
- }
1229
1626
  function undeclaredFieldOnStrictIndex(field, option) {
1230
1627
  return new NarsilError(
1231
1628
  ErrorCodes.SEARCH_INVALID_FIELD,
@@ -1233,34 +1630,14 @@ function undeclaredFieldOnStrictIndex(field, option) {
1233
1630
  { field, option }
1234
1631
  );
1235
1632
  }
1236
- function requireValidExpression(expression, rules, depth) {
1237
- if (depth > MAX_FILTER_DEPTH) invalidShape(`A filter expression nests at most ${MAX_FILTER_DEPTH} levels deep`);
1238
- if (!isObjectRecord(expression)) invalidShape("Each filter expression must be an object");
1239
- const { fields, and, or, not } = expression;
1240
- if (fields !== void 0) {
1241
- if (!isObjectRecord(fields)) invalidShape('"fields" in a filter expression must be an object keyed by field path');
1242
- for (const [fieldPath, filter] of Object.entries(fields)) {
1243
- if (rules.declaredOnly && !Object.hasOwn(rules.fieldTypes, fieldPath)) {
1244
- throw undeclaredFieldOnStrictIndex(fieldPath, "filter");
1245
- }
1246
- if (isObjectRecord(filter)) requireValidOperands(fieldPath, filter, rules.fieldTypes[fieldPath]);
1247
- }
1248
- }
1249
- for (const [clause, list] of [
1250
- ["and", and],
1251
- ["or", or]
1252
- ]) {
1253
- if (list === void 0) continue;
1254
- if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
1255
- for (const nested of list) requireValidExpression(nested, rules, depth + 1);
1256
- }
1257
- if (not !== void 0) requireValidExpression(not, rules, depth + 1);
1258
- }
1259
- function requireValidFilter(expression, fieldTypes, declaredOnly = false) {
1260
- if (expression === void 0 || expression === null) return;
1261
- requireValidExpression(expression, { fieldTypes, declaredOnly }, 0);
1633
+ function undeclaredFilterField(field) {
1634
+ return new NarsilError(
1635
+ ErrorCodes.SEARCH_INVALID_FIELD,
1636
+ `The schema declares no field "${field}", while the engine answers a filter only from the indexes that it keeps for declared fields. Declare the field in the schema of a new index to filter by it`,
1637
+ { field, option: "filter" }
1638
+ );
1262
1639
  }
1263
- function requireValidOperands(fieldPath, filter, type) {
1640
+ function requireRangeOperands(fieldPath, filter, type) {
1264
1641
  for (const operator of RANGE_OPERATORS) {
1265
1642
  const bound = filter[operator];
1266
1643
  if (bound === void 0) continue;
@@ -1273,22 +1650,23 @@ function requireValidOperands(fieldPath, filter, type) {
1273
1650
  }
1274
1651
  requireFieldKind(fieldPath, operator, type, ["number"]);
1275
1652
  }
1276
- if (filter.between !== void 0) {
1277
- const bounds = filter.between;
1278
- const isPair = Array.isArray(bounds) && bounds.length === 2;
1279
- if (isPair && typeof bounds[0] === "string" && typeof bounds[1] === "string") {
1280
- requireTextRangeField(fieldPath, "between", type);
1281
- } else {
1282
- if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
1283
- invalid(
1284
- `"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
1285
- fieldPath,
1286
- "between"
1287
- );
1288
- }
1289
- requireFieldKind(fieldPath, "between", type, ["number"]);
1290
- }
1653
+ if (filter.between === void 0) return;
1654
+ const bounds = filter.between;
1655
+ const isPair = Array.isArray(bounds) && bounds.length === 2;
1656
+ if (isPair && typeof bounds[0] === "string" && typeof bounds[1] === "string") {
1657
+ requireTextRangeField(fieldPath, "between", type);
1658
+ return;
1291
1659
  }
1660
+ if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
1661
+ invalid(
1662
+ `"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
1663
+ fieldPath,
1664
+ "between"
1665
+ );
1666
+ }
1667
+ requireFieldKind(fieldPath, "between", type, ["number"]);
1668
+ }
1669
+ function requireValueOperands(fieldPath, filter, type) {
1292
1670
  for (const operator of TEXT_OPERATORS) {
1293
1671
  if (filter[operator] === void 0) continue;
1294
1672
  if (typeof filter[operator] !== "string") {
@@ -1314,22 +1692,81 @@ function requireValidOperands(fieldPath, filter, type) {
1314
1692
  if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
1315
1693
  invalid(`"${operator}" on field "${fieldPath}" must be a list of strings`, fieldPath, operator);
1316
1694
  }
1695
+ requireListLength(fieldPath, operator, values);
1317
1696
  requireFieldKind(fieldPath, operator, type, ["text"]);
1318
1697
  }
1319
- if (filter.size !== void 0) {
1320
- const size = filter.size;
1321
- if (size === null || typeof size !== "object") {
1322
- invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
1323
- }
1324
- requireValidOperands(`${fieldPath}.size`, size, "number");
1698
+ for (const operator of GEO_OPERATORS) {
1699
+ if (filter[operator] !== void 0) requireFieldKind(fieldPath, operator, type, ["geopoint"]);
1325
1700
  }
1326
- if (filter.radius !== void 0) requireRadius(fieldPath, filter.radius);
1327
- if (filter.polygon !== void 0) {
1328
- if (!isObjectRecord(filter.polygon)) {
1329
- invalid(`"polygon" on field "${fieldPath}" must be an object`, fieldPath, "polygon");
1330
- }
1331
- requirePolygonRing(filter.polygon.points);
1701
+ }
1702
+ function requireValidOperands(fieldPath, filter, type) {
1703
+ requireWellFormedText(fieldPath, filter);
1704
+ requireRangeOperands(fieldPath, filter, type);
1705
+ requireValueOperands(fieldPath, filter, type);
1706
+ requireWholeValueIndex(fieldPath, filter, type);
1707
+ requirePresenceOperands(fieldPath, filter);
1708
+ requireElementOperands(fieldPath, filter, type);
1709
+ requireSizeOperands(fieldPath, filter, type);
1710
+ requirePatternOperands(fieldPath, filter, type);
1711
+ requireGeoOperands(fieldPath, filter);
1712
+ }
1713
+ function filterTestCount(expression) {
1714
+ if (!isObjectRecord(expression)) return 0;
1715
+ const fieldFilters = isObjectRecord(expression.fields) ? Object.values(expression.fields) : [];
1716
+ let tests = fieldFilters.length === 0 && !holdsNestedExpressions(expression) ? 1 : 0;
1717
+ for (const filter of fieldFilters) tests += isObjectRecord(filter) ? testsIn(filter) : 1;
1718
+ for (const list of [expression.and, expression.or]) {
1719
+ if (Array.isArray(list)) for (const nested of list) tests += filterTestCount(nested);
1720
+ }
1721
+ return tests + filterTestCount(expression.not);
1722
+ }
1723
+ function testsIn(filter) {
1724
+ let tests = 0;
1725
+ for (const [operator, operand] of Object.entries(filter)) {
1726
+ if (operator === "caseFold") continue;
1727
+ tests += operator === "size" && isObjectRecord(operand) ? Math.max(1, Object.keys(operand).length) : 1;
1728
+ }
1729
+ return Math.max(1, tests);
1730
+ }
1731
+ function countTests(walk, tests) {
1732
+ walk.tests += tests;
1733
+ if (walk.tests > MAX_FILTER_TESTS) {
1734
+ invalidShape(`A filter holds at most ${MAX_FILTER_TESTS} tests across all of its fields and nested expressions`);
1735
+ }
1736
+ }
1737
+ function holdsNestedExpressions(expression) {
1738
+ const { and, or, not } = expression;
1739
+ return Array.isArray(and) && and.length > 0 || Array.isArray(or) && or.length > 0 || not !== void 0;
1740
+ }
1741
+ function requireValidExpression(expression, walk, depth) {
1742
+ if (depth > MAX_FILTER_DEPTH) invalidShape(`A filter expression nests at most ${MAX_FILTER_DEPTH} levels deep`);
1743
+ if (!isObjectRecord(expression)) invalidShape("Each filter expression must be an object");
1744
+ const { fields, and, or, not } = expression;
1745
+ if (fields !== void 0 && !isObjectRecord(fields)) {
1746
+ invalidShape('"fields" in a filter expression must be an object keyed by field path');
1747
+ }
1748
+ const fieldFilters = Object.entries(fields ?? {});
1749
+ if (fieldFilters.length === 0 && !holdsNestedExpressions(expression)) countTests(walk, 1);
1750
+ for (const [fieldPath, filter] of fieldFilters) {
1751
+ const type = walk.fieldTypes[fieldPath];
1752
+ if (!Object.hasOwn(walk.fieldTypes, fieldPath) || type === void 0) throw undeclaredFilterField(fieldPath);
1753
+ if (!isObjectRecord(filter)) invalidShape(`The filter on field "${fieldPath}" must be an object of tests`);
1754
+ countTests(walk, testsIn(filter));
1755
+ requireValidOperands(fieldPath, filter, type);
1756
+ }
1757
+ for (const [clause, list] of [
1758
+ ["and", and],
1759
+ ["or", or]
1760
+ ]) {
1761
+ if (list === void 0) continue;
1762
+ if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
1763
+ for (const nested of list) requireValidExpression(nested, walk, depth + 1);
1332
1764
  }
1765
+ if (not !== void 0) requireValidExpression(not, walk, depth + 1);
1766
+ }
1767
+ function requireValidFilter(expression, fieldTypes) {
1768
+ if (expression === void 0 || expression === null) return;
1769
+ requireValidExpression(expression, { fieldTypes, tests: 0 }, 0);
1333
1770
  }
1334
1771
 
1335
1772
  // src/search/sorting.ts
@@ -1679,7 +2116,11 @@ function prepareFulltextQuery(partition, params, language, schema, options) {
1679
2116
  }
1680
2117
  let filterBitset;
1681
2118
  if (params.filters) {
1682
- filterBitset = partition.applyFiltersBitset(params.filters, schema);
2119
+ filterBitset = partition.applyFiltersBitset(
2120
+ params.filters,
2121
+ schema,
2122
+ options?.patternWork ?? createPatternWorkMeter()
2123
+ );
1683
2124
  if (bitsetIsEmpty(filterBitset)) return null;
1684
2125
  }
1685
2126
  return { queryTokens, prefixExpansion, filterBitset };
@@ -1922,7 +2363,8 @@ function resolveEffectiveMode(config) {
1922
2363
  function buildSearchOptions(base, globalStats) {
1923
2364
  return {
1924
2365
  ...base,
1925
- globalStats: globalStats ?? base?.globalStats
2366
+ globalStats: globalStats ?? base?.globalStats,
2367
+ patternWork: base?.patternWork ?? createPatternWorkMeter()
1926
2368
  };
1927
2369
  }
1928
2370
  function dispatchWithDispatcher(partitions, params, language, schema, options, dispatcher) {
@@ -1949,8 +2391,12 @@ function countsWithoutScores(params) {
1949
2391
  }
1950
2392
  function fanOutMatchCount(manager, params, language, schema, options) {
1951
2393
  let count = 0;
2394
+ const searchOptions = {
2395
+ ...options?.searchOptions,
2396
+ patternWork: options?.searchOptions?.patternWork ?? createPatternWorkMeter()
2397
+ };
1952
2398
  for (const partition of partitionsIn(manager, options?.partitionIds)) {
1953
- const matches = fulltextMatches(partition, params, language, schema, options?.searchOptions);
2399
+ const matches = fulltextMatches(partition, params, language, schema, searchOptions);
1954
2400
  if (matches !== null) count += matches.count;
1955
2401
  }
1956
2402
  return count;
@@ -2132,4 +2578,4 @@ function setNestedValue(obj, path, value) {
2132
2578
  current[segments[segments.length - 1]] = value;
2133
2579
  }
2134
2580
 
2135
- export { DEG_TO_RAD, MAX_FILTER_DEPTH, MIN_POLYGON_POINTS, VINCENTY_CONVERGENCE_THRESHOLD, VINCENTY_MAX_ITERATIONS, applyPagination, applyProjection, applySorting, bitsetAnd, bitsetFromSet, bitsetHas, bitsetNot, bitsetOr, bitsetSet, boundedLevenshtein, clampRowCount, clearNormalizationCache, cloneProjected, collectQueryTermStats, computeFacets, configureNormalizationCache, countsWithoutScores, createBitSet, decodePageCursor, encodePageCursor, everyValueFacetConfig, fanOutMatchCount, fanOutQuery, fnv1a, fnv1aBytes, fnv1aUint32s, fulltextMatches, fuzzyPrefixOf, fuzzyTermMatches, getAllInternalDocIds, getFieldValueByInternalId, getFlatSchema, getNestedValue, getNormalizationCacheSize, getOrCreateFieldNameIndex, kWayMerge, mergeFacets, mergePartitionStats, normalizeSort, oversampledFacetConfig, oversampledShardSize, partitionsIn, polygonContainment, projectionKeepsField, pruneStatsToQueryTerms, readSortValues, requireMatchingCursor, requirePolygonRing, requireSortableFields, requireValidFilter, requireValidOperands, requireWithinResultWindow, resetNormalizationCache, resolveProjection, sanitizeGlobalStats, setNestedValue, sortModesOf, sortSignatureEntry, sortSignatureOf, textFieldsChanged, tokenize, tokenizeIterator, tokenizeOptions, undeclaredFieldOnStrictIndex, visitTokens };
2581
+ export { GEO_UNIT_METRES, MAX_FILTER_ARRAY_SIZE, MAX_FILTER_DEPTH, MAX_FILTER_TESTS, applyPagination, applyProjection, applySorting, boundedLevenshtein, clampRowCount, clearNormalizationCache, cloneProjected, collectQueryTermStats, computeFacets, configureNormalizationCache, countsWithoutScores, coveringRanges, createPatternWorkMeter, decodePageCursor, encodePageCursor, everyValueFacetConfig, fanOutMatchCount, fanOutQuery, filterTestCount, fnv1a, fnv1aBytes, fnv1aUint32s, fulltextMatches, fuzzyPrefixOf, fuzzyTermMatches, getFieldValueByInternalId, getFlatSchema, getNestedValue, getNormalizationCacheSize, getOrCreateFieldNameIndex, joinPatternWorkMeter, kWayMerge, locationCode, mergeFacets, mergePartitionStats, normalizeSort, oversampledFacetConfig, oversampledShardSize, parseVectorDimension, partitionsIn, polygonBoxes, polygonContainment, projectionKeepsField, pruneStatsToQueryTerms, radiusBoxes, readSortValues, requireMatchingCursor, requireSortableFields, requireValidFilter, requireValidOperands, requireWithinResultWindow, resetNormalizationCache, resolveProjection, sanitizeGlobalStats, setNestedValue, sortModesOf, sortSignatureEntry, sortSignatureOf, textFieldsChanged, tokenize, tokenizeIterator, tokenizeOptions, undeclaredFieldOnStrictIndex, undeclaredFilterField, visitTokens };