@delali/narsil 0.3.1-next.70 → 0.3.1-next.72

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 (57) hide show
  1. package/dist/adapters/filesystem.mjs +1 -1
  2. package/dist/adapters/indexeddb.mjs +1 -1
  3. package/dist/{capabilities-Dt-7B4Wz.d.ts → capabilities-D6gjjQyA.d.ts} +2 -2
  4. package/dist/{chunk-P4DSQ6M4.mjs → chunk-2X2ZQOAB.mjs} +69 -28
  5. package/dist/chunk-3HQPUCCT.mjs +785 -0
  6. package/dist/{chunk-VNMTB3KC.mjs → chunk-7QMTA7AA.mjs} +194 -59
  7. package/dist/{chunk-FVLQ63B7.mjs → chunk-CVPA5Q7R.mjs} +24 -3
  8. package/dist/{chunk-2STVUTNA.mjs → chunk-EDMCZLSJ.mjs} +4 -3
  9. package/dist/{chunk-M76V3OGJ.mjs → chunk-HVJYTKK7.mjs} +940 -647
  10. package/dist/{chunk-HWQLTLNG.mjs → chunk-KZTI25D3.mjs} +1 -1
  11. package/dist/{chunk-YJDQ7NLA.mjs → chunk-LCCL6EEH.mjs} +3 -3
  12. package/dist/{chunk-OI42GWLR.mjs → chunk-LJYTW7GT.mjs} +57 -11
  13. package/dist/{chunk-653BNMAF.mjs → chunk-M6XF5HDE.mjs} +57 -17
  14. package/dist/{chunk-YNVIUGF5.mjs → chunk-P2ZZN32C.mjs} +1 -1
  15. package/dist/{chunk-LMO5TJUS.mjs → chunk-QR4OKRVY.mjs} +637 -196
  16. package/dist/{chunk-USKFU7J5.mjs → chunk-RV77FR2F.mjs} +1 -0
  17. package/dist/{chunk-GIVAGD3Q.mjs → chunk-XZ4PAREY.mjs} +1 -1
  18. package/dist/{chunk-3JGWULDF.mjs → chunk-Y52WZWAU.mjs} +5 -5
  19. package/dist/chunk-ZKRV5M2P.mjs +75 -0
  20. package/dist/{client-COek5GXK.d.ts → client-C2TiW29y.d.ts} +3 -3
  21. package/dist/client.d.ts +8 -8
  22. package/dist/client.mjs +1 -0
  23. package/dist/{config-rBpbgTw8.d.ts → config-BLguB-om.d.ts} +2 -2
  24. package/dist/{config-Dfanyqqt.d.ts → config-C7Ip8Ife.d.ts} +17 -2
  25. package/dist/direct-executor-SVCHXR74.mjs +14 -0
  26. package/dist/distribution/coordinator/etcd.d.ts +2 -2
  27. package/dist/distribution/coordinator/etcd.mjs +2 -2
  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.mjs +1 -1
  31. package/dist/distribution.d.ts +8 -8
  32. package/dist/distribution.mjs +58 -35
  33. package/dist/embeddings/openai.mjs +1 -1
  34. package/dist/{engine-CXKfmK7I.d.ts → engine-Dm4446j1.d.ts} +104 -62
  35. package/dist/index.browser.mjs +2222 -1100
  36. package/dist/index.d.ts +27 -8
  37. package/dist/index.mjs +14 -12
  38. package/dist/invalidation/filesystem.mjs +1 -1
  39. package/dist/{memory-C7UMCMH3.d.ts → memory-CKHTU1CL.d.ts} +66 -25
  40. package/dist/pattern-Cledccbl.d.ts +18 -0
  41. package/dist/pattern.d.ts +22 -0
  42. package/dist/pattern.mjs +7 -0
  43. package/dist/persistence/durability/checkpoint-worker.d.ts +17 -1
  44. package/dist/persistence/durability/checkpoint-worker.mjs +14 -12
  45. package/dist/react.d.ts +277 -182
  46. package/dist/react.mjs +190 -45
  47. package/dist/{schema-Bgzr4g7b.d.ts → schema-BBOCdhND.d.ts} +9 -7
  48. package/dist/server.d.ts +10 -10
  49. package/dist/server.mjs +13 -12
  50. package/dist/{tasks-BZt_zes6.d.ts → tasks-BECd0b__.d.ts} +1 -1
  51. package/dist/{thread-JEVAYETR.mjs → thread-IBSIZZ6W.mjs} +22 -13
  52. package/dist/{types-gOUqhs2F.d.ts → types-BiUmaiWT.d.ts} +1 -1
  53. package/dist/vector/search-worker.d.ts +1 -1
  54. package/dist/vector/search-worker.mjs +4 -4
  55. package/dist/workers/entry.mjs +4 -4
  56. package/package.json +15 -7
  57. package/dist/direct-executor-GYJEKNV5.mjs +0 -12
@@ -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-M6XF5HDE.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-RV77FR2F.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);
@@ -800,13 +834,6 @@ function getFieldValueByInternalId(docStore, internalId, fieldPath) {
800
834
  if (externalId === void 0) return void 0;
801
835
  return getFieldValueForDoc(docStore, externalId, fieldPath);
802
836
  }
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
837
 
811
838
  // src/core/partition/facets.ts
812
839
  var UNDECLARED_FIELD_TYPE = "";
@@ -1084,13 +1111,205 @@ function decodeCursorText(encoded) {
1084
1111
  return new TextDecoder().decode(bytes);
1085
1112
  }
1086
1113
 
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;
1114
+ // src/filters/constants.ts
1115
+ var MAX_FILTER_DEPTH = 30;
1116
+ var MAX_FILTER_TESTS = 1024;
1117
+ var MAX_FILTER_ARRAY_SIZE = 65536;
1118
+ var GEO_UNIT_METRES = { km: 1e3, mi: 1609.344, m: 1 };
1119
+
1120
+ // src/filters/operands/checks.ts
1121
+ function operandKindOf(type) {
1122
+ if (type === void 0) return void 0;
1123
+ if (type === "number" || type === "number[]") return "number";
1124
+ if (type === "boolean" || type === "boolean[]") return "boolean";
1125
+ if (type === "geopoint") return "geopoint";
1126
+ if (type.startsWith("vector[")) return "vector";
1127
+ return "text";
1128
+ }
1129
+ function isFiniteNumber(value) {
1130
+ return typeof value === "number" && Number.isFinite(value);
1131
+ }
1132
+ function kindOfValue(value) {
1133
+ if (isFiniteNumber(value)) return "number";
1134
+ if (typeof value === "boolean") return "boolean";
1135
+ if (typeof value === "string") return "text";
1136
+ return void 0;
1137
+ }
1138
+ function isObjectRecord(value) {
1139
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1140
+ }
1141
+ function invalid(message, fieldPath, operator) {
1142
+ throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message, { fieldPath, operator });
1143
+ }
1144
+ function requireFieldKind(fieldPath, operator, type, accepted) {
1145
+ const kind = operandKindOf(type);
1146
+ if (kind === void 0 || accepted.includes(kind)) return;
1147
+ invalid(
1148
+ `The engine applies "${operator}" to ${accepted.join(" or ")} fields alone, and the schema declares field "${fieldPath}" as "${type}"`,
1149
+ fieldPath,
1150
+ operator
1151
+ );
1152
+ }
1153
+ function requireTextRangeField(fieldPath, operator, type) {
1154
+ const parsed = parsedTypeOf(type);
1155
+ if (parsed?.sortable && (parsed.base === "string" || parsed.base === "verbatim")) return;
1156
+ const declared = type === void 0 ? `the schema has no field "${fieldPath}"` : `field "${fieldPath}" has type "${type}"`;
1157
+ invalid(
1158
+ `The engine applies "${operator}" with a text bound to a "string" or "verbatim" field whose type includes "sortable", but ${declared}`,
1159
+ fieldPath,
1160
+ operator
1161
+ );
1162
+ }
1163
+ function requireListLength(fieldPath, operator, values) {
1164
+ if (values.length <= MAX_FILTER_ARRAY_SIZE) return;
1165
+ invalid(
1166
+ `"${operator}" on field "${fieldPath}" holds ${values.length} values, while a filter list holds at most ${MAX_FILTER_ARRAY_SIZE}`,
1167
+ fieldPath,
1168
+ operator
1169
+ );
1170
+ }
1171
+
1172
+ // src/geo/cells.ts
1173
+ var AXIS_CELLS = 2 ** LOCATION_CODE_BITS_PER_AXIS;
1174
+ function axisCell(value, lowest, span) {
1175
+ const cell = Math.floor((value - lowest) / span * AXIS_CELLS);
1176
+ if (!(cell > 0)) return 0;
1177
+ return cell >= AXIS_CELLS ? AXIS_CELLS - 1 : cell;
1178
+ }
1179
+ function latitudeCell(lat) {
1180
+ return axisCell(lat, -MAX_LATITUDE_DEGREES, 2 * MAX_LATITUDE_DEGREES);
1181
+ }
1182
+ function longitudeCell(lon) {
1183
+ return axisCell(lon, -MAX_LONGITUDE_DEGREES, FULL_TURN_DEGREES);
1184
+ }
1185
+ function interleave(row, column, bits) {
1186
+ let code = 0;
1187
+ for (let bit = bits - 1; bit >= 0; bit--) code = code * 4 + (row >>> bit & 1) * 2 + (column >>> bit & 1);
1188
+ return code;
1189
+ }
1190
+ function locationCode(lat, lon) {
1191
+ return interleave(latitudeCell(lat), longitudeCell(lon), LOCATION_CODE_BITS_PER_AXIS);
1192
+ }
1193
+ function appendBoxRanges(box, out) {
1194
+ const south = latitudeCell(box.south);
1195
+ const north = latitudeCell(box.north);
1196
+ const west = longitudeCell(box.west);
1197
+ const east = longitudeCell(box.east);
1198
+ let level = LOCATION_CODE_BITS_PER_AXIS;
1199
+ while (level > 0) {
1200
+ const shift2 = LOCATION_CODE_BITS_PER_AXIS - level;
1201
+ const rows = (north >>> shift2) - (south >>> shift2) + 1;
1202
+ const columns = (east >>> shift2) - (west >>> shift2) + 1;
1203
+ if (rows * columns <= MAX_COVERING_CELLS) break;
1204
+ level--;
1205
+ }
1206
+ const shift = LOCATION_CODE_BITS_PER_AXIS - level;
1207
+ const codesPerCell = 4 ** shift;
1208
+ for (let row = south >>> shift; row <= north >>> shift; row++) {
1209
+ for (let column = west >>> shift; column <= east >>> shift; column++) {
1210
+ const from = interleave(row, column, level) * codesPerCell;
1211
+ out.push({ from, to: from + codesPerCell });
1212
+ }
1213
+ }
1214
+ }
1215
+ function coveringRanges(boxes) {
1216
+ const ranges = [];
1217
+ for (const box of boxes) appendBoxRanges(box, ranges);
1218
+ ranges.sort((left, right) => left.from - right.from);
1219
+ const merged = [];
1220
+ for (const range of ranges) {
1221
+ const last = merged[merged.length - 1];
1222
+ if (last !== void 0 && range.from <= last.to) {
1223
+ if (range.to > last.to) last.to = range.to;
1224
+ } else {
1225
+ merged.push({ from: range.from, to: range.to });
1226
+ }
1227
+ }
1228
+ return merged;
1229
+ }
1230
+ function longitudeBoxes(south, north, west, east) {
1231
+ if (east - west >= FULL_TURN_DEGREES) {
1232
+ return [{ south, north, west: -MAX_LONGITUDE_DEGREES, east: MAX_LONGITUDE_DEGREES }];
1233
+ }
1234
+ if (west < -MAX_LONGITUDE_DEGREES) {
1235
+ return [
1236
+ { south, north, west: west + FULL_TURN_DEGREES, east: MAX_LONGITUDE_DEGREES },
1237
+ { south, north, west: -MAX_LONGITUDE_DEGREES, east }
1238
+ ];
1239
+ }
1240
+ if (east > MAX_LONGITUDE_DEGREES) {
1241
+ return [
1242
+ { south, north, west, east: MAX_LONGITUDE_DEGREES },
1243
+ { south, north, west: -MAX_LONGITUDE_DEGREES, east: east - FULL_TURN_DEGREES }
1244
+ ];
1245
+ }
1246
+ return [{ south, north, west, east }];
1247
+ }
1248
+ function radiusBoxes(lat, lon, distanceMeters) {
1249
+ const angle = distanceMeters / EARTH_RADIUS_METERS * RADIUS_BOX_MARGIN_RATIO + RADIUS_BOX_MARGIN_RADIANS;
1250
+ const angleDegrees = angle / DEG_TO_RAD;
1251
+ const south = lat - angleDegrees;
1252
+ const north = lat + angleDegrees;
1253
+ if (angle >= Math.PI || south <= -MAX_LATITUDE_DEGREES || north >= MAX_LATITUDE_DEGREES) {
1254
+ return [
1255
+ {
1256
+ south: Math.max(south, -MAX_LATITUDE_DEGREES),
1257
+ north: Math.min(north, MAX_LATITUDE_DEGREES),
1258
+ west: -MAX_LONGITUDE_DEGREES,
1259
+ east: MAX_LONGITUDE_DEGREES
1260
+ }
1261
+ ];
1262
+ }
1263
+ const spread = Math.asin(Math.sin(angle) / Math.cos(lat * DEG_TO_RAD)) / DEG_TO_RAD;
1264
+ return longitudeBoxes(south, north, lon - spread, lon + spread);
1265
+ }
1266
+
1267
+ // src/geo/edge-tree.ts
1268
+ function buildLongitudeEdgeTree(lons) {
1269
+ const n = lons.length;
1270
+ const edges = [];
1271
+ for (let i = 0; i < n; i++) {
1272
+ if (lons[i] !== lons[i === 0 ? n - 1 : i - 1]) edges.push(i);
1273
+ }
1274
+ const lowOf = (i) => Math.min(lons[i], lons[i === 0 ? n - 1 : i - 1]);
1275
+ edges.sort((left, right) => lowOf(left) - lowOf(right));
1276
+ const count = edges.length;
1277
+ const tree = {
1278
+ from: new Int32Array(count),
1279
+ to: new Int32Array(count),
1280
+ low: new Float64Array(count),
1281
+ high: new Float64Array(count),
1282
+ subtreeHigh: new Float64Array(count)
1283
+ };
1284
+ edges.forEach((i, at) => {
1285
+ const j = i === 0 ? n - 1 : i - 1;
1286
+ tree.from[at] = i;
1287
+ tree.to[at] = j;
1288
+ tree.low[at] = Math.min(lons[i], lons[j]);
1289
+ tree.high[at] = Math.max(lons[i], lons[j]);
1290
+ });
1291
+ const fillSubtreeHigh = (start, end) => {
1292
+ if (start >= end) return Number.NEGATIVE_INFINITY;
1293
+ const mid = start + end >>> 1;
1294
+ const highest = Math.max(tree.high[mid], fillSubtreeHigh(start, mid), fillSubtreeHigh(mid + 1, end));
1295
+ tree.subtreeHigh[mid] = highest;
1296
+ return highest;
1297
+ };
1298
+ fillSubtreeHigh(0, count);
1299
+ return tree;
1300
+ }
1301
+ function forEachEdgeSpanning(tree, lon, visit) {
1302
+ const walk = (start, end) => {
1303
+ if (start >= end) return;
1304
+ const mid = start + end >>> 1;
1305
+ if (tree.subtreeHigh[mid] <= lon) return;
1306
+ walk(start, mid);
1307
+ if (tree.low[mid] > lon) return;
1308
+ if (lon < tree.high[mid]) visit(tree.from[mid], tree.to[mid]);
1309
+ walk(mid + 1, end);
1310
+ };
1311
+ walk(0, tree.from.length);
1312
+ }
1094
1313
 
1095
1314
  // src/geo/polygon.ts
1096
1315
  function requirePolygonRing(points) {
@@ -1103,11 +1322,18 @@ function requirePolygonRing(points) {
1103
1322
  }
1104
1323
  points.forEach((point, position) => {
1105
1324
  const { lat, lon } = point ?? {};
1106
- if (typeof lat === "number" && Number.isFinite(lat) && typeof lon === "number" && Number.isFinite(lon)) return;
1325
+ if (typeof lat !== "number" || !Number.isFinite(lat) || typeof lon !== "number" || !Number.isFinite(lon)) {
1326
+ throw new NarsilError(
1327
+ ErrorCodes.SEARCH_INVALID_FILTER,
1328
+ `A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
1329
+ { position }
1330
+ );
1331
+ }
1332
+ if (isLatitude(lat) && isLongitude(lon)) return;
1107
1333
  throw new NarsilError(
1108
1334
  ErrorCodes.SEARCH_INVALID_FILTER,
1109
- `A polygon point carries a finite "lat" and "lon", and the point at position ${position} does not`,
1110
- { position }
1335
+ `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}`,
1336
+ { position, lat, lon }
1111
1337
  );
1112
1338
  });
1113
1339
  }
@@ -1140,67 +1366,40 @@ function polygonContainment(polygon) {
1140
1366
  lats[i] = polygon[i].lat;
1141
1367
  lons[i] = unwrap ? eastward(polygon[i].lon) : polygon[i].lon;
1142
1368
  }
1369
+ const edges = buildLongitudeEdgeTree(lons);
1143
1370
  return (lat, rawLon) => {
1144
1371
  const lon = unwrap ? eastward(rawLon) : rawLon;
1145
1372
  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
- }
1373
+ forEachEdgeSpanning(edges, lon, (i, j) => {
1374
+ const intersectLat = lats[i] + (lats[j] - lats[i]) / (lons[j] - lons[i]) * (lon - lons[i]);
1375
+ if (lat < intersectLat) inside = !inside;
1376
+ });
1154
1377
  return inside;
1155
1378
  };
1156
1379
  }
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
- );
1380
+ function polygonBoxes(polygon) {
1381
+ if (polygon.length < MIN_POLYGON_POINTS) return [];
1382
+ const unwrap = crossesAntimeridian(polygon);
1383
+ let south = Number.POSITIVE_INFINITY;
1384
+ let north = Number.NEGATIVE_INFINITY;
1385
+ let west = Number.POSITIVE_INFINITY;
1386
+ let east = Number.NEGATIVE_INFINITY;
1387
+ for (const point of polygon) {
1388
+ const lon = unwrap ? eastward(point.lon) : point.lon;
1389
+ south = Math.min(south, point.lat);
1390
+ north = Math.max(north, point.lat);
1391
+ west = Math.min(west, lon);
1392
+ east = Math.max(east, lon);
1393
+ }
1394
+ if (west >= MAX_LONGITUDE_DEGREES)
1395
+ return longitudeBoxes(south, north, west - FULL_TURN_DEGREES, east - FULL_TURN_DEGREES);
1396
+ return longitudeBoxes(south, north, west, east);
1194
1397
  }
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
- );
1398
+
1399
+ // src/filters/operands/geo.ts
1400
+ function requireFlag(fieldPath, operator, flag, value) {
1401
+ if (value === void 0 || typeof value === "boolean") return;
1402
+ invalid(`"${operator}.${flag}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
1204
1403
  }
1205
1404
  function requireRadius(fieldPath, radius) {
1206
1405
  const circle = radius;
@@ -1212,6 +1411,15 @@ function requireRadius(fieldPath, radius) {
1212
1411
  invalid(`"radius.${key}" on field "${fieldPath}" must be a finite number`, fieldPath, "radius");
1213
1412
  }
1214
1413
  }
1414
+ if (typeof circle.unit !== "string" || !Object.hasOwn(GEO_UNIT_METRES, circle.unit)) {
1415
+ invalid(
1416
+ `"radius.unit" on field "${fieldPath}" must be one of ${Object.keys(GEO_UNIT_METRES).join(", ")}, and this filter sets ${String(circle.unit)}`,
1417
+ fieldPath,
1418
+ "radius"
1419
+ );
1420
+ }
1421
+ requireFlag(fieldPath, "radius", "inside", circle.inside);
1422
+ requireFlag(fieldPath, "radius", "highPrecision", circle.highPrecision);
1215
1423
  if (circle.distance < 0) {
1216
1424
  invalid(
1217
1425
  `"radius.distance" on field "${fieldPath}" must be zero or more, and this filter sets ${String(circle.distance)}`,
@@ -1219,13 +1427,197 @@ function requireRadius(fieldPath, radius) {
1219
1427
  "radius"
1220
1428
  );
1221
1429
  }
1430
+ if (!isLatitude(circle.lat)) {
1431
+ invalid(
1432
+ `"radius.lat" on field "${fieldPath}" must be from -${MAX_LATITUDE_DEGREES} to ${MAX_LATITUDE_DEGREES}, and this filter sets ${String(circle.lat)}`,
1433
+ fieldPath,
1434
+ "radius"
1435
+ );
1436
+ }
1437
+ if (!isLongitude(circle.lon)) {
1438
+ invalid(
1439
+ `"radius.lon" on field "${fieldPath}" must be from -${MAX_LONGITUDE_DEGREES} to ${MAX_LONGITUDE_DEGREES}, and this filter sets ${String(circle.lon)}`,
1440
+ fieldPath,
1441
+ "radius"
1442
+ );
1443
+ }
1444
+ }
1445
+ function requireGeoOperands(fieldPath, filter) {
1446
+ if (filter.radius !== void 0) requireRadius(fieldPath, filter.radius);
1447
+ if (filter.polygon === void 0) return;
1448
+ if (!isObjectRecord(filter.polygon)) {
1449
+ invalid(`"polygon" on field "${fieldPath}" must be an object`, fieldPath, "polygon");
1450
+ }
1451
+ requireFlag(fieldPath, "polygon", "inside", filter.polygon.inside);
1452
+ const points = filter.polygon.points;
1453
+ if (Array.isArray(points)) requireListLength(fieldPath, "polygon", points);
1454
+ requirePolygonRing(points);
1455
+ }
1456
+
1457
+ // src/filters/operands/lists.ts
1458
+ var WHOLE_VALUE_OPERATORS = ["eq", "ne", "in", "nin", "startsWith", "endsWith", "matchesAny", "containsAll"];
1459
+ var ELEMENT_OPERATORS = ["matchesAny", "containsAll"];
1460
+ var PRESENCE_OPERATORS = ["exists", "notExists", "isEmpty", "isNotEmpty"];
1461
+ var SIZE_OPERATORS = ["eq", "ne", "gt", "gte", "lt", "lte", "between"];
1462
+ var SIZE_OPERATOR_SET = new Set(SIZE_OPERATORS);
1463
+ var ELEMENT_KIND_BY_BASE = {
1464
+ "number[]": "number",
1465
+ "boolean[]": "boolean",
1466
+ "enum[]": "text",
1467
+ "string[]": "text",
1468
+ "verbatim[]": "text"
1469
+ };
1470
+ function requireWholeValueIndex(fieldPath, filter, type) {
1471
+ const parsed = parsedTypeOf(type);
1472
+ if (parsed === void 0 || parsed.pattern || parsed.base !== "string" && parsed.base !== "string[]") return;
1473
+ for (const operator of WHOLE_VALUE_OPERATORS) {
1474
+ if (filter[operator] === void 0) continue;
1475
+ invalid(
1476
+ `"${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`,
1477
+ fieldPath,
1478
+ operator
1479
+ );
1480
+ }
1481
+ }
1482
+ function requirePresenceOperands(fieldPath, filter) {
1483
+ for (const operator of PRESENCE_OPERATORS) {
1484
+ const operand = filter[operator];
1485
+ if (operand !== void 0 && typeof operand !== "boolean") {
1486
+ invalid(`"${operator}" on field "${fieldPath}" must be true or false`, fieldPath, operator);
1487
+ }
1488
+ }
1489
+ }
1490
+ function listElementKind(fieldPath, operator, type) {
1491
+ const base = parsedTypeOf(type)?.base;
1492
+ const kind = base === void 0 ? void 0 : ELEMENT_KIND_BY_BASE[base];
1493
+ if (kind !== void 0) return kind;
1494
+ return invalid(
1495
+ `"${operator}" tests the elements of a list field, while the schema declares field "${fieldPath}" as "${type}"`,
1496
+ fieldPath,
1497
+ operator
1498
+ );
1499
+ }
1500
+ function requireElementOperands(fieldPath, filter, type) {
1501
+ for (const operator of ELEMENT_OPERATORS) {
1502
+ const values = filter[operator];
1503
+ if (values === void 0) continue;
1504
+ if (!Array.isArray(values)) invalid(`"${operator}" on field "${fieldPath}" must be a list`, fieldPath, operator);
1505
+ requireListLength(fieldPath, operator, values);
1506
+ const kind = listElementKind(fieldPath, operator, type);
1507
+ if (values.some((value) => kindOfValue(value) !== kind)) {
1508
+ invalid(
1509
+ `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`,
1510
+ fieldPath,
1511
+ operator
1512
+ );
1513
+ }
1514
+ }
1515
+ }
1516
+ function requireSizeOperands(fieldPath, filter, type) {
1517
+ const size = filter.size;
1518
+ if (size === void 0) return;
1519
+ if (!isObjectRecord(size)) invalid(`"size" on field "${fieldPath}" must be a comparison object`, fieldPath, "size");
1520
+ listElementKind(fieldPath, "size", type);
1521
+ for (const [operator, operand] of Object.entries(size)) {
1522
+ if (!SIZE_OPERATOR_SET.has(operator)) {
1523
+ invalid(
1524
+ `"size" on field "${fieldPath}" takes ${SIZE_OPERATORS.join(", ")}, while "${operator}" is none of them`,
1525
+ fieldPath,
1526
+ "size"
1527
+ );
1528
+ }
1529
+ const bounds = operator === "between" ? operand : [operand];
1530
+ if (!Array.isArray(bounds) || bounds.length !== (operator === "between" ? 2 : 1) || !bounds.every(isFiniteNumber)) {
1531
+ invalid(
1532
+ `"size.${operator}" on field "${fieldPath}" must be ${operator === "between" ? "two finite numbers, the lower bound first" : "a finite number"}`,
1533
+ fieldPath,
1534
+ "size"
1535
+ );
1536
+ }
1537
+ }
1222
1538
  }
1539
+
1540
+ // src/filters/operands/pattern.ts
1541
+ function requirePatternField(fieldPath, operator, type) {
1542
+ if (parsedTypeOf(type)?.pattern === true) return;
1543
+ const declared = type === void 0 ? `The schema declares no field "${fieldPath}"` : `Field "${fieldPath}" has type "${type}"`;
1544
+ invalid(
1545
+ `"${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}`,
1546
+ fieldPath,
1547
+ operator
1548
+ );
1549
+ }
1550
+ function requirePatternText(fieldPath, operator, text, caseFold) {
1551
+ if (exceedsCodePoints(text, MAX_PATTERN_TEXT_CODE_POINTS)) {
1552
+ invalid(
1553
+ `The text of "${operator}" on pattern field "${fieldPath}" holds more than ${MAX_PATTERN_TEXT_CODE_POINTS} code points`,
1554
+ fieldPath,
1555
+ operator
1556
+ );
1557
+ }
1558
+ if (caseFold && foldedCodePointCount(text) > MAX_PATTERN_POSITIONS) {
1559
+ invalid(
1560
+ `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`,
1561
+ fieldPath,
1562
+ operator
1563
+ );
1564
+ }
1565
+ }
1566
+ var ELEMENT_OPERATORS2 = ["matchesAny", "containsAll"];
1567
+ function requirePatternOperands(fieldPath, filter, type) {
1568
+ if (filter.contains !== void 0) {
1569
+ if (typeof filter.contains !== "string") {
1570
+ invalid(`"contains" on field "${fieldPath}" must be a string`, fieldPath, "contains");
1571
+ }
1572
+ requirePatternField(fieldPath, "contains", type);
1573
+ }
1574
+ if (filter.caseFold !== void 0) {
1575
+ if (typeof filter.caseFold !== "boolean") {
1576
+ invalid(`"caseFold" on field "${fieldPath}" must be true or false`, fieldPath, "caseFold");
1577
+ }
1578
+ requirePatternField(fieldPath, "caseFold", type);
1579
+ }
1580
+ if (parsedTypeOf(type)?.pattern !== true) return;
1581
+ const caseFold = filter.caseFold === true;
1582
+ for (const operator of PATTERN_TEST_OPERATORS) {
1583
+ const operand = filter[operator];
1584
+ if (typeof operand === "string") requirePatternText(fieldPath, operator, operand, caseFold);
1585
+ if (!Array.isArray(operand)) continue;
1586
+ for (const text of operand) {
1587
+ if (typeof text === "string") requirePatternText(fieldPath, operator, text, caseFold);
1588
+ }
1589
+ }
1590
+ for (const operator of ELEMENT_OPERATORS2) {
1591
+ const operand = filter[operator];
1592
+ if (!Array.isArray(operand)) continue;
1593
+ for (const text of operand) {
1594
+ if (typeof text === "string") requirePatternText(fieldPath, operator, text, false);
1595
+ }
1596
+ }
1597
+ }
1598
+ function requireWellFormedText(fieldPath, filter) {
1599
+ for (const [operator, operand] of Object.entries(filter)) {
1600
+ const texts = Array.isArray(operand) ? operand : [operand];
1601
+ for (const text of texts) {
1602
+ if (typeof text !== "string" || !holdsUnpairedSurrogate(text)) continue;
1603
+ invalid(
1604
+ `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`,
1605
+ fieldPath,
1606
+ operator
1607
+ );
1608
+ }
1609
+ }
1610
+ }
1611
+
1612
+ // src/filters/operands/index.ts
1613
+ var RANGE_OPERATORS = ["gt", "lt", "gte", "lte"];
1614
+ var TEXT_OPERATORS = ["startsWith", "endsWith"];
1615
+ var EQUALITY_OPERATORS = ["eq", "ne"];
1616
+ var LIST_OPERATORS = ["in", "nin"];
1617
+ var GEO_OPERATORS = ["radius", "polygon"];
1223
1618
  function invalidShape(message) {
1224
1619
  throw new NarsilError(ErrorCodes.SEARCH_INVALID_FILTER, message);
1225
1620
  }
1226
- function isObjectRecord(value) {
1227
- return value !== null && typeof value === "object" && !Array.isArray(value);
1228
- }
1229
1621
  function undeclaredFieldOnStrictIndex(field, option) {
1230
1622
  return new NarsilError(
1231
1623
  ErrorCodes.SEARCH_INVALID_FIELD,
@@ -1233,34 +1625,14 @@ function undeclaredFieldOnStrictIndex(field, option) {
1233
1625
  { field, option }
1234
1626
  );
1235
1627
  }
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);
1628
+ function undeclaredFilterField(field) {
1629
+ return new NarsilError(
1630
+ ErrorCodes.SEARCH_INVALID_FIELD,
1631
+ `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`,
1632
+ { field, option: "filter" }
1633
+ );
1262
1634
  }
1263
- function requireValidOperands(fieldPath, filter, type) {
1635
+ function requireRangeOperands(fieldPath, filter, type) {
1264
1636
  for (const operator of RANGE_OPERATORS) {
1265
1637
  const bound = filter[operator];
1266
1638
  if (bound === void 0) continue;
@@ -1273,22 +1645,23 @@ function requireValidOperands(fieldPath, filter, type) {
1273
1645
  }
1274
1646
  requireFieldKind(fieldPath, operator, type, ["number"]);
1275
1647
  }
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
- }
1648
+ if (filter.between === void 0) return;
1649
+ const bounds = filter.between;
1650
+ const isPair = Array.isArray(bounds) && bounds.length === 2;
1651
+ if (isPair && typeof bounds[0] === "string" && typeof bounds[1] === "string") {
1652
+ requireTextRangeField(fieldPath, "between", type);
1653
+ return;
1291
1654
  }
1655
+ if (!isPair || !isFiniteNumber(bounds[0]) || !isFiniteNumber(bounds[1])) {
1656
+ invalid(
1657
+ `"between" on field "${fieldPath}" must be two finite numbers or two strings, the lower bound first`,
1658
+ fieldPath,
1659
+ "between"
1660
+ );
1661
+ }
1662
+ requireFieldKind(fieldPath, "between", type, ["number"]);
1663
+ }
1664
+ function requireValueOperands(fieldPath, filter, type) {
1292
1665
  for (const operator of TEXT_OPERATORS) {
1293
1666
  if (filter[operator] === void 0) continue;
1294
1667
  if (typeof filter[operator] !== "string") {
@@ -1314,22 +1687,81 @@ function requireValidOperands(fieldPath, filter, type) {
1314
1687
  if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) {
1315
1688
  invalid(`"${operator}" on field "${fieldPath}" must be a list of strings`, fieldPath, operator);
1316
1689
  }
1690
+ requireListLength(fieldPath, operator, values);
1317
1691
  requireFieldKind(fieldPath, operator, type, ["text"]);
1318
1692
  }
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");
1693
+ for (const operator of GEO_OPERATORS) {
1694
+ if (filter[operator] !== void 0) requireFieldKind(fieldPath, operator, type, ["geopoint"]);
1325
1695
  }
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);
1696
+ }
1697
+ function requireValidOperands(fieldPath, filter, type) {
1698
+ requireWellFormedText(fieldPath, filter);
1699
+ requireRangeOperands(fieldPath, filter, type);
1700
+ requireValueOperands(fieldPath, filter, type);
1701
+ requireWholeValueIndex(fieldPath, filter, type);
1702
+ requirePresenceOperands(fieldPath, filter);
1703
+ requireElementOperands(fieldPath, filter, type);
1704
+ requireSizeOperands(fieldPath, filter, type);
1705
+ requirePatternOperands(fieldPath, filter, type);
1706
+ requireGeoOperands(fieldPath, filter);
1707
+ }
1708
+ function filterTestCount(expression) {
1709
+ if (!isObjectRecord(expression)) return 0;
1710
+ const fieldFilters = isObjectRecord(expression.fields) ? Object.values(expression.fields) : [];
1711
+ let tests = fieldFilters.length === 0 && !holdsNestedExpressions(expression) ? 1 : 0;
1712
+ for (const filter of fieldFilters) tests += isObjectRecord(filter) ? testsIn(filter) : 1;
1713
+ for (const list of [expression.and, expression.or]) {
1714
+ if (Array.isArray(list)) for (const nested of list) tests += filterTestCount(nested);
1715
+ }
1716
+ return tests + filterTestCount(expression.not);
1717
+ }
1718
+ function testsIn(filter) {
1719
+ let tests = 0;
1720
+ for (const [operator, operand] of Object.entries(filter)) {
1721
+ if (operator === "caseFold") continue;
1722
+ tests += operator === "size" && isObjectRecord(operand) ? Math.max(1, Object.keys(operand).length) : 1;
1723
+ }
1724
+ return Math.max(1, tests);
1725
+ }
1726
+ function countTests(walk, tests) {
1727
+ walk.tests += tests;
1728
+ if (walk.tests > MAX_FILTER_TESTS) {
1729
+ invalidShape(`A filter holds at most ${MAX_FILTER_TESTS} tests across all of its fields and nested expressions`);
1730
+ }
1731
+ }
1732
+ function holdsNestedExpressions(expression) {
1733
+ const { and, or, not } = expression;
1734
+ return Array.isArray(and) && and.length > 0 || Array.isArray(or) && or.length > 0 || not !== void 0;
1735
+ }
1736
+ function requireValidExpression(expression, walk, depth) {
1737
+ if (depth > MAX_FILTER_DEPTH) invalidShape(`A filter expression nests at most ${MAX_FILTER_DEPTH} levels deep`);
1738
+ if (!isObjectRecord(expression)) invalidShape("Each filter expression must be an object");
1739
+ const { fields, and, or, not } = expression;
1740
+ if (fields !== void 0 && !isObjectRecord(fields)) {
1741
+ invalidShape('"fields" in a filter expression must be an object keyed by field path');
1742
+ }
1743
+ const fieldFilters = Object.entries(fields ?? {});
1744
+ if (fieldFilters.length === 0 && !holdsNestedExpressions(expression)) countTests(walk, 1);
1745
+ for (const [fieldPath, filter] of fieldFilters) {
1746
+ const type = walk.fieldTypes[fieldPath];
1747
+ if (!Object.hasOwn(walk.fieldTypes, fieldPath) || type === void 0) throw undeclaredFilterField(fieldPath);
1748
+ if (!isObjectRecord(filter)) invalidShape(`The filter on field "${fieldPath}" must be an object of tests`);
1749
+ countTests(walk, testsIn(filter));
1750
+ requireValidOperands(fieldPath, filter, type);
1751
+ }
1752
+ for (const [clause, list] of [
1753
+ ["and", and],
1754
+ ["or", or]
1755
+ ]) {
1756
+ if (list === void 0) continue;
1757
+ if (!Array.isArray(list)) invalidShape(`"${clause}" in a filter expression must be a list of filter expressions`);
1758
+ for (const nested of list) requireValidExpression(nested, walk, depth + 1);
1332
1759
  }
1760
+ if (not !== void 0) requireValidExpression(not, walk, depth + 1);
1761
+ }
1762
+ function requireValidFilter(expression, fieldTypes) {
1763
+ if (expression === void 0 || expression === null) return;
1764
+ requireValidExpression(expression, { fieldTypes, tests: 0 }, 0);
1333
1765
  }
1334
1766
 
1335
1767
  // src/search/sorting.ts
@@ -1679,7 +2111,11 @@ function prepareFulltextQuery(partition, params, language, schema, options) {
1679
2111
  }
1680
2112
  let filterBitset;
1681
2113
  if (params.filters) {
1682
- filterBitset = partition.applyFiltersBitset(params.filters, schema);
2114
+ filterBitset = partition.applyFiltersBitset(
2115
+ params.filters,
2116
+ schema,
2117
+ options?.patternWork ?? createPatternWorkMeter()
2118
+ );
1683
2119
  if (bitsetIsEmpty(filterBitset)) return null;
1684
2120
  }
1685
2121
  return { queryTokens, prefixExpansion, filterBitset };
@@ -1922,7 +2358,8 @@ function resolveEffectiveMode(config) {
1922
2358
  function buildSearchOptions(base, globalStats) {
1923
2359
  return {
1924
2360
  ...base,
1925
- globalStats: globalStats ?? base?.globalStats
2361
+ globalStats: globalStats ?? base?.globalStats,
2362
+ patternWork: base?.patternWork ?? createPatternWorkMeter()
1926
2363
  };
1927
2364
  }
1928
2365
  function dispatchWithDispatcher(partitions, params, language, schema, options, dispatcher) {
@@ -1949,8 +2386,12 @@ function countsWithoutScores(params) {
1949
2386
  }
1950
2387
  function fanOutMatchCount(manager, params, language, schema, options) {
1951
2388
  let count = 0;
2389
+ const searchOptions = {
2390
+ ...options?.searchOptions,
2391
+ patternWork: options?.searchOptions?.patternWork ?? createPatternWorkMeter()
2392
+ };
1952
2393
  for (const partition of partitionsIn(manager, options?.partitionIds)) {
1953
- const matches = fulltextMatches(partition, params, language, schema, options?.searchOptions);
2394
+ const matches = fulltextMatches(partition, params, language, schema, searchOptions);
1954
2395
  if (matches !== null) count += matches.count;
1955
2396
  }
1956
2397
  return count;
@@ -2132,4 +2573,4 @@ function setNestedValue(obj, path, value) {
2132
2573
  current[segments[segments.length - 1]] = value;
2133
2574
  }
2134
2575
 
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 };
2576
+ 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, 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 };