@effect-dynamodb/geo 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Justin Menga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @effect-dynamodb/geo
2
+
3
+ Geospatial index and proximity search for [`effect-dynamodb`](https://www.npmjs.com/package/effect-dynamodb), built on Uber's [H3](https://h3geo.org) hexagonal grid.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@effect-dynamodb/geo)](https://www.npmjs.com/package/@effect-dynamodb/geo)
6
+ [![license](https://img.shields.io/npm/l/@effect-dynamodb/geo)](./LICENSE)
7
+
8
+ **Documentation:** https://jmenga.github.io/effect-dynamodb/guides/geospatial
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pnpm add @effect-dynamodb/geo effect-dynamodb effect
14
+ ```
15
+
16
+ `effect` and `effect-dynamodb` are peer dependencies.
17
+
18
+ ## What it does
19
+
20
+ Adds two operations to any entity with `lat`/`lng` coordinates:
21
+
22
+ - **Proximity search** — "find items within N kilometers of a point"
23
+ - **Bounding-box search** — "find items inside a rectangle"
24
+
25
+ Implemented as a GSI overlay using H3 cell IDs as the partition key, so reads stay O(visited cells) instead of scanning the table.
26
+
27
+ See the [Geospatial guide](https://jmenga.github.io/effect-dynamodb/guides/geospatial) for setup, indexing strategy, and tradeoffs.
28
+
29
+ ## License
30
+
31
+ [MIT](./LICENSE)
@@ -0,0 +1,128 @@
1
+ /**
2
+ * GeoIndex — Geospatial index for effect-dynamodb entities.
3
+ *
4
+ * Binds geospatial configuration to an Entity and provides:
5
+ * - `put` — write with automatic geo field enrichment
6
+ * - `nearby` — radius-based proximity search
7
+ * - `enrich` — lower-level geo field computation (for transactions/batch writes)
8
+ */
9
+ import { type Context, Effect, type Schema } from "effect";
10
+ import type { DynamoClient, DynamoClientError, DynamoSchema, KeyComposer, Table, UniqueConstraintViolation, ValidationError } from "effect-dynamodb";
11
+ import * as H3 from "./H3.js";
12
+ import type { LatLng } from "./Spherical.js";
13
+ export type { LatLng } from "./Spherical.js";
14
+ export type TimeBucket = "hourly";
15
+ export type SortOrder = "ASC" | "DESC";
16
+ export interface GeoFields {
17
+ readonly cell: {
18
+ readonly field: string;
19
+ readonly resolution: number;
20
+ };
21
+ readonly parentCell: {
22
+ readonly field: string;
23
+ readonly resolution: number;
24
+ };
25
+ readonly timePartition: {
26
+ readonly field: string;
27
+ readonly source: string;
28
+ readonly bucket: TimeBucket;
29
+ };
30
+ }
31
+ export interface Coordinates {
32
+ readonly latitude: number;
33
+ readonly longitude: number;
34
+ }
35
+ export interface NearbyOptions {
36
+ readonly center: LatLng;
37
+ readonly radius: number;
38
+ readonly unit?: H3.GeoUnit | undefined;
39
+ readonly timeWindow?: {
40
+ readonly start: number;
41
+ readonly end: number;
42
+ } | undefined;
43
+ readonly sort?: SortOrder | undefined;
44
+ readonly pkFilter?: Record<string, unknown> | undefined;
45
+ }
46
+ export interface NearbyResult<A> {
47
+ readonly item: A;
48
+ readonly distance: number;
49
+ }
50
+ /** Structural type for the entity properties GeoIndex needs */
51
+ export interface GeoEntity<A, P> {
52
+ readonly _schema: DynamoSchema.DynamoSchema;
53
+ readonly _tableTag: Context.Service<Table.TableConfig, Table.TableConfig>;
54
+ readonly entityType: string;
55
+ readonly indexes: Record<string, KeyComposer.IndexDefinition>;
56
+ readonly schemas: {
57
+ readonly recordSchema: Schema.Codec<any>;
58
+ };
59
+ readonly put: (input: A) => P;
60
+ }
61
+ export interface GeoIndex<A, P> {
62
+ /** Write an item with automatic geo field enrichment. */
63
+ readonly put: (input: A) => P;
64
+ /** Search for items near a geographic point within a given radius. */
65
+ readonly nearby: (options: NearbyOptions) => Effect.Effect<Array<NearbyResult<A>>, DynamoClientError | ValidationError, DynamoClient | Table.TableConfig>;
66
+ /** Compute geo fields for an input (for use with transactions/batch writes). */
67
+ readonly enrich: (input: A) => A;
68
+ }
69
+ /**
70
+ * Create a GeoIndex that binds geospatial configuration to an entity's GSI.
71
+ *
72
+ * Returns an object with `put`, `nearby`, and `enrich` methods.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const VehicleGeo = GeoIndex.make({
77
+ * entity: Vehicles,
78
+ * index: "byCell",
79
+ * coordinates: (item) => item.latitude !== undefined && item.longitude !== undefined
80
+ * ? { latitude: item.latitude, longitude: item.longitude }
81
+ * : undefined,
82
+ * fields: {
83
+ * cell: { field: "cell", resolution: 15 },
84
+ * parentCell: { field: "parentCell", resolution: 3 },
85
+ * timePartition: { field: "timePartition", source: "timestamp", bucket: "hourly" },
86
+ * },
87
+ * })
88
+ *
89
+ * // Write — geo fields computed automatically
90
+ * yield* VehicleGeo.put({ vehicleId: "v-1", latitude: 37.77, longitude: -122.42, timestamp: now })
91
+ *
92
+ * // Search
93
+ * const results = yield* VehicleGeo.nearby({ center, radius: 2000, unit: "m" })
94
+ * ```
95
+ */
96
+ export declare const make: <A, P>(config: {
97
+ readonly entity: GeoEntity<A, P>;
98
+ readonly index: string;
99
+ readonly coordinates: (item: A) => Coordinates | undefined;
100
+ readonly fields: GeoFields;
101
+ }) => GeoIndex<A, P>;
102
+ /**
103
+ * A GeoIndex with `DynamoClient | Table.TableConfig` dependencies resolved.
104
+ * All operations return `Effect<..., ..., never>`.
105
+ */
106
+ export interface BoundGeoIndex<A> {
107
+ /** Write an item with automatic geo field enrichment. R = never. */
108
+ readonly put: (input: A) => Effect.Effect<A, DynamoClientError | ValidationError | UniqueConstraintViolation, never>;
109
+ /** Search for items near a geographic point. R = never. */
110
+ readonly nearby: (options: NearbyOptions) => Effect.Effect<Array<NearbyResult<A>>, DynamoClientError | ValidationError, never>;
111
+ /** Compute geo fields for an input (pure — same as GeoIndex.enrich). */
112
+ readonly enrich: (input: A) => A;
113
+ /** Provide escape hatch for complex pipe chains. */
114
+ readonly provide: <B, E>(effect: Effect.Effect<B, E, DynamoClient | Table.TableConfig>) => Effect.Effect<B, E, never>;
115
+ }
116
+ /**
117
+ * Resolve `DynamoClient | Table.TableConfig` dependencies and return a
118
+ * `BoundGeoIndex` whose operations all have `R = never`.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const geo = yield* GeoIndex.bind(VehicleGeo)
123
+ * yield* geo.put({ vehicleId: "v-1", latitude: 37.77, longitude: -122.42, timestamp: Date.now() })
124
+ * const results = yield* geo.nearby({ center, radius: 2000, unit: "m" })
125
+ * ```
126
+ */
127
+ export declare const bind: <A, P>(geoIndex: GeoIndex<A, P>) => Effect.Effect<BoundGeoIndex<A>, never, DynamoClient | Table.TableConfig>;
128
+ //# sourceMappingURL=GeoIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoIndex.d.ts","sourceRoot":"","sources":["../src/GeoIndex.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC1D,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,KAAK,EACL,yBAAyB,EACzB,eAAe,EAChB,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAA;AACjC,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,CAAA;AAMtC,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IACtE,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5E,QAAQ,CAAC,aAAa,EAAE;QACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;KAC5B,CAAA;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,OAAO,GAAG,SAAS,CAAA;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAClF,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;CACxD;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,+DAA+D;AAC/D,MAAM,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,YAAY,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,CAAC,CAAA;IAC7D,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;KAAE,CAAA;IAC9D,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;CAC9B;AAED,MAAM,WAAW,QAAQ,CAAC,CAAC,EAAE,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,CACf,OAAO,EAAE,aAAa,KACnB,MAAM,CAAC,MAAM,CAChB,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EACtB,iBAAiB,GAAG,eAAe,EACnC,YAAY,GAAG,KAAK,CAAC,WAAW,CACjC,CAAA;IACD,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,CAAC,EAAE,QAAQ;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,WAAW,GAAG,SAAS,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;CAC3B,KAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CA0ChB,CAAA;AAMD;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,GAAG,EAAE,CACZ,KAAK,EAAE,CAAC,KACL,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,iBAAiB,GAAG,eAAe,GAAG,yBAAyB,EAAE,KAAK,CAAC,CAAA;IAC7F,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,CACf,OAAO,EAAE,aAAa,KACnB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,iBAAiB,GAAG,eAAe,EAAE,KAAK,CAAC,CAAA;IACtF,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;IAChC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC,KAC1D,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;CAChC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,CAAC,EACvB,UAAU,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,KACvB,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,GAAG,KAAK,CAAC,WAAW,CAatE,CAAA"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * GeoIndex — Geospatial index for effect-dynamodb entities.
3
+ *
4
+ * Binds geospatial configuration to an Entity and provides:
5
+ * - `put` — write with automatic geo field enrichment
6
+ * - `nearby` — radius-based proximity search
7
+ * - `enrich` — lower-level geo field computation (for transactions/batch writes)
8
+ */
9
+ import { Effect } from "effect";
10
+ import * as _GeoSearch from "./GeoSearch.js";
11
+ import * as H3 from "./H3.js";
12
+ const BUCKET_MS = {
13
+ hourly: H3.HOURLY_BUCKET_MS,
14
+ };
15
+ /**
16
+ * Create a GeoIndex that binds geospatial configuration to an entity's GSI.
17
+ *
18
+ * Returns an object with `put`, `nearby`, and `enrich` methods.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const VehicleGeo = GeoIndex.make({
23
+ * entity: Vehicles,
24
+ * index: "byCell",
25
+ * coordinates: (item) => item.latitude !== undefined && item.longitude !== undefined
26
+ * ? { latitude: item.latitude, longitude: item.longitude }
27
+ * : undefined,
28
+ * fields: {
29
+ * cell: { field: "cell", resolution: 15 },
30
+ * parentCell: { field: "parentCell", resolution: 3 },
31
+ * timePartition: { field: "timePartition", source: "timestamp", bucket: "hourly" },
32
+ * },
33
+ * })
34
+ *
35
+ * // Write — geo fields computed automatically
36
+ * yield* VehicleGeo.put({ vehicleId: "v-1", latitude: 37.77, longitude: -122.42, timestamp: now })
37
+ *
38
+ * // Search
39
+ * const results = yield* VehicleGeo.nearby({ center, radius: 2000, unit: "m" })
40
+ * ```
41
+ */
42
+ export const make = (config) => {
43
+ const indexDef = config.entity.indexes[config.index];
44
+ if (!indexDef) {
45
+ throw new Error(`GeoIndex: index "${config.index}" not found on entity`);
46
+ }
47
+ const bucketMs = BUCKET_MS[config.fields.timePartition.bucket];
48
+ const enrich = (input) => {
49
+ const coords = config.coordinates(input);
50
+ if (!coords)
51
+ return input;
52
+ const { fields } = config;
53
+ const cell = H3.computeCell(coords.latitude, coords.longitude, fields.cell.resolution);
54
+ const parentCell = H3.computeParentCell(cell, fields.parentCell.resolution);
55
+ const record = input;
56
+ const timestampValue = record[fields.timePartition.source];
57
+ if (typeof timestampValue !== "number")
58
+ return input;
59
+ const timePartition = H3.computeTimePartition(timestampValue, bucketMs);
60
+ return {
61
+ ...input,
62
+ [fields.cell.field]: cell,
63
+ [fields.parentCell.field]: parentCell,
64
+ [fields.timePartition.field]: timePartition,
65
+ };
66
+ };
67
+ const searchConfig = {
68
+ entity: config.entity,
69
+ indexDef,
70
+ coordinates: config.coordinates,
71
+ fields: config.fields,
72
+ bucketMs,
73
+ };
74
+ return {
75
+ put: (input) => config.entity.put(enrich(input)),
76
+ nearby: (options) => _GeoSearch.nearby(searchConfig, options),
77
+ enrich,
78
+ };
79
+ };
80
+ /**
81
+ * Resolve `DynamoClient | Table.TableConfig` dependencies and return a
82
+ * `BoundGeoIndex` whose operations all have `R = never`.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const geo = yield* GeoIndex.bind(VehicleGeo)
87
+ * yield* geo.put({ vehicleId: "v-1", latitude: 37.77, longitude: -122.42, timestamp: Date.now() })
88
+ * const results = yield* geo.nearby({ center, radius: 2000, unit: "m" })
89
+ * ```
90
+ */
91
+ export const bind = (geoIndex) => Effect.gen(function* () {
92
+ const ctx = yield* Effect.context();
93
+ const provide = (effect) => Effect.provide(effect, ctx);
94
+ return {
95
+ put: (input) => provide(geoIndex.put(input).asEffect()),
96
+ nearby: (options) => provide(geoIndex.nearby(options)),
97
+ enrich: geoIndex.enrich,
98
+ provide,
99
+ };
100
+ });
101
+ //# sourceMappingURL=GeoIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoIndex.js","sourceRoot":"","sources":["../src/GeoIndex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * GeoSearch — Internal geospatial search orchestration for DynamoDB.
3
+ *
4
+ * Composes multiple Query objects from effect-dynamodb core and executes them
5
+ * in parallel to perform radius-based proximity searches using H3 hexagonal grid.
6
+ *
7
+ * @internal Used by GeoIndex — consumers should use `GeoIndex.nearby()`.
8
+ */
9
+ import { type Context, Effect, Schema } from "effect";
10
+ import type { DynamoClientError, DynamoSchema, Table } from "effect-dynamodb";
11
+ import { type DynamoClient, KeyComposer, ValidationError } from "effect-dynamodb";
12
+ import type { Coordinates, GeoFields, NearbyOptions, NearbyResult } from "./GeoIndex.js";
13
+ /** Internal config passed from GeoIndex.make to the search implementation. */
14
+ export interface SearchConfig<A> {
15
+ readonly entity: {
16
+ readonly _schema: DynamoSchema.DynamoSchema;
17
+ readonly _tableTag: Context.Service<Table.TableConfig, Table.TableConfig>;
18
+ readonly entityType: string;
19
+ readonly indexes: Record<string, KeyComposer.IndexDefinition>;
20
+ readonly schemas: {
21
+ readonly recordSchema: Schema.Codec<any>;
22
+ };
23
+ };
24
+ readonly indexDef: KeyComposer.IndexDefinition;
25
+ readonly coordinates: (item: A) => Coordinates | undefined;
26
+ readonly fields: GeoFields;
27
+ readonly bucketMs: number;
28
+ }
29
+ /**
30
+ * Search for items near a geographic point within a given radius.
31
+ *
32
+ * Orchestrates:
33
+ * 1. Compute optimal H3 resolution and ring size for the radius
34
+ * 2. Generate and prune H3 cells within radius
35
+ * 3. Group contiguous cells into BETWEEN range query chunks
36
+ * 4. Generate time partition keys for the time window
37
+ * 5. Build N queries (one per timePartition x chunk combination)
38
+ * 6. Execute all queries in parallel
39
+ * 7. Post-process: compute great-circle distance, filter by radius, sort
40
+ */
41
+ export declare const nearby: <A>(config: SearchConfig<A>, options: NearbyOptions) => Effect.Effect<Array<NearbyResult<A>>, DynamoClientError | ValidationError, DynamoClient | Table.TableConfig>;
42
+ //# sourceMappingURL=GeoSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoSearch.d.ts","sourceRoot":"","sources":["../src/GeoSearch.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAC7E,OAAO,EAAE,KAAK,YAAY,EAAE,WAAW,EAAS,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAExF,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAIxF,8EAA8E;AAC9E,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,YAAY,CAAA;QAC3C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;QACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;QAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,CAAC,CAAA;QAC7D,QAAQ,CAAC,OAAO,EAAE;YAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;SAAE,CAAA;KAC/D,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAA;IAC9C,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,WAAW,GAAG,SAAS,CAAA;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM,GAAI,CAAC,EACtB,QAAQ,YAAY,CAAC,CAAC,CAAC,EACvB,SAAS,aAAa,KACrB,MAAM,CAAC,MAAM,CACd,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EACtB,iBAAiB,GAAG,eAAe,EACnC,YAAY,GAAG,KAAK,CAAC,WAAW,CAmI9B,CAAA"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * GeoSearch — Internal geospatial search orchestration for DynamoDB.
3
+ *
4
+ * Composes multiple Query objects from effect-dynamodb core and executes them
5
+ * in parallel to perform radius-based proximity searches using H3 hexagonal grid.
6
+ *
7
+ * @internal Used by GeoIndex — consumers should use `GeoIndex.nearby()`.
8
+ */
9
+ import { Effect, Schema } from "effect";
10
+ import { KeyComposer, Query, ValidationError } from "effect-dynamodb";
11
+ import * as h3 from "h3-js";
12
+ import * as H3 from "./H3.js";
13
+ import * as Spherical from "./Spherical.js";
14
+ const H3_RESOLUTION_PRECISE = 15;
15
+ /**
16
+ * Search for items near a geographic point within a given radius.
17
+ *
18
+ * Orchestrates:
19
+ * 1. Compute optimal H3 resolution and ring size for the radius
20
+ * 2. Generate and prune H3 cells within radius
21
+ * 3. Group contiguous cells into BETWEEN range query chunks
22
+ * 4. Generate time partition keys for the time window
23
+ * 5. Build N queries (one per timePartition x chunk combination)
24
+ * 6. Execute all queries in parallel
25
+ * 7. Post-process: compute great-circle distance, filter by radius, sort
26
+ */
27
+ export const nearby = (config, options) => Effect.gen(function* () {
28
+ const { center, radius, unit = "m", sort = "ASC", pkFilter } = options;
29
+ const { latitude, longitude } = center;
30
+ // Step 1: Optimal H3 resolution for the search radius
31
+ const searchResolution = H3.optimalResolution(latitude, longitude, radius);
32
+ const cell = H3.computeCell(latitude, longitude, searchResolution);
33
+ // Step 2: Number of rings needed
34
+ const k = H3.optimalK(cell, radius, unit);
35
+ // Step 3: Generate grid disk (concentric rings)
36
+ const ringCells = h3.gridDiskDistances(cell, k);
37
+ // Step 4: Prune cells outside radius
38
+ const prunedCells = H3.pruneCells(latitude, longitude, radius, ringCells);
39
+ // Step 5: Group into contiguous chunks for BETWEEN queries
40
+ const sortedChunks = H3.sequentialChunk(prunedCells.flat());
41
+ // Step 6: Time partitions
42
+ const now = Date.now();
43
+ const startTime = options.timeWindow?.start ?? now - 15 * 60 * 1000;
44
+ const endTime = options.timeWindow?.end ?? now;
45
+ const timePartitions = H3.getTimePartitions(startTime, endTime, config.bucketMs);
46
+ // Step 7: Build queries — one per (timePartition, chunk)
47
+ const { entity, indexDef, fields } = config;
48
+ const dynamoIndexName = indexDef.index;
49
+ const pkField = indexDef.pk.field;
50
+ const skField = indexDef.sk.field;
51
+ const resolveTableName = entity._tableTag.useSync((tc) => tc.name);
52
+ const decoder = (raw) => Schema.decodeUnknownEffect(entity.schemas.recordSchema)(raw).pipe(Effect.mapError((cause) => new ValidationError({
53
+ entityType: entity.entityType,
54
+ operation: "GeoSearch.decode",
55
+ cause,
56
+ })));
57
+ // Group search cells by parent cell — when search resolution is low,
58
+ // multiple search cells may map to the same parent cell at parentResolution
59
+ const parentResolution = fields.parentCell.resolution;
60
+ const queryConfigs = timePartitions.flatMap((partition) => sortedChunks
61
+ .map((chunk) => {
62
+ const firstCell = chunk[0];
63
+ const lastCell = chunk[chunk.length - 1];
64
+ if (!firstCell || !lastCell)
65
+ return undefined;
66
+ // Compute parent cell: if search resolution > parent resolution, use cellToParent.
67
+ // Otherwise, the search cell IS at or below parent resolution — use it directly.
68
+ const parentCell = searchResolution > parentResolution
69
+ ? H3.computeParentCell(firstCell, parentResolution)
70
+ : firstCell;
71
+ const lowerCell = H3.cellToCenterChild(firstCell, H3_RESOLUTION_PRECISE);
72
+ const upperCell = H3.cellToUpperBound(lastCell, H3_RESOLUTION_PRECISE);
73
+ // Build PK composites: geo fields + any extra filter composites
74
+ const pkComposites = {
75
+ ...pkFilter,
76
+ [fields.parentCell.field]: parentCell,
77
+ [fields.timePartition.field]: partition,
78
+ };
79
+ const pkValue = KeyComposer.composePk(entity._schema, entity.entityType, indexDef, pkComposites);
80
+ // Compose full SK values with schema prefix (must match stored SK format)
81
+ const lower = KeyComposer.composeSk(entity._schema, entity.entityType, 1, indexDef, {
82
+ [fields.cell.field]: lowerCell,
83
+ });
84
+ const upper = KeyComposer.composeSk(entity._schema, entity.entityType, 1, indexDef, {
85
+ [fields.cell.field]: upperCell,
86
+ });
87
+ return { pkValue, lower, upper };
88
+ })
89
+ .filter((q) => q !== undefined));
90
+ // Step 8: Build and execute all queries in parallel
91
+ const allItems = yield* Effect.forEach(queryConfigs, (qc) => {
92
+ const query = Query.make({
93
+ tableName: "",
94
+ indexName: dynamoIndexName,
95
+ pkField,
96
+ pkValue: qc.pkValue,
97
+ skField,
98
+ entityTypes: [entity.entityType],
99
+ decoder: decoder,
100
+ resolveTableName,
101
+ }).pipe(Query.where({ between: [qc.lower, qc.upper] }));
102
+ return Query.collect(query);
103
+ }, { concurrency: "unbounded" });
104
+ const flatItems = allItems.flat();
105
+ // Step 9: Compute distance, filter by radius, sort
106
+ const results = [];
107
+ for (const item of flatItems) {
108
+ const coords = config.coordinates(item);
109
+ if (!coords)
110
+ continue;
111
+ const distance = Spherical.greatCircleDistance(center, coords);
112
+ if (unit === "km" ? distance / 1000 <= radius : distance <= radius) {
113
+ results.push({ item, distance: unit === "km" ? distance / 1000 : distance });
114
+ }
115
+ }
116
+ results.sort((a, b) => a.distance - b.distance);
117
+ return sort === "ASC" ? results : results.reverse();
118
+ });
119
+ //# sourceMappingURL=GeoSearch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoSearch.js","sourceRoot":"","sources":["../src/GeoSearch.ts"],"names":[],"mappings":"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"}
package/dist/H3.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * H3 — Pure H3 hexagonal grid algorithms for geospatial indexing.
3
+ *
4
+ * Ported from midgard's TelemetryRepository. All functions are pure
5
+ * (no Effect dependencies) and operate on H3 cells and coordinates.
6
+ */
7
+ export type GeoUnit = "m" | "km";
8
+ /**
9
+ * Find the H3 resolution where hexagon edge length best matches the search radius.
10
+ *
11
+ * Starts with an estimate from average edge lengths, then refines using
12
+ * actual edge lengths at the given location (hexagons vary by latitude).
13
+ */
14
+ export declare const optimalResolution: (latitude: number, longitude: number, radius: number) => number;
15
+ /**
16
+ * Calculate the number of hexagonal rings (k) needed to cover a given radius
17
+ * from a center cell.
18
+ */
19
+ export declare const optimalK: (cell: string, radius: number, unit: GeoUnit) => number;
20
+ /**
21
+ * Compute the distance from a point to the edge between two neighboring cells.
22
+ *
23
+ * Uses spherical geometry to find the intersection of the bearing from
24
+ * the target cell center toward the search point with the shared edge boundary.
25
+ */
26
+ export declare const edgeDistance: (latitude: number, longitude: number, sourceCell: string, targetCell: string) => number;
27
+ /**
28
+ * Remove cells from outer rings that are beyond the search radius.
29
+ *
30
+ * Walks outward ring by ring, checking edge distance from the search center
31
+ * to each cell's shared boundary with its inner-ring neighbor.
32
+ */
33
+ export declare const pruneCells: (latitude: number, longitude: number, radius: number, cells: string[][]) => string[][];
34
+ /**
35
+ * Group sorted H3 cells into chunks of contiguous cells.
36
+ *
37
+ * Contiguous cells (adjacent in the H3 index space) can be queried
38
+ * with a single BETWEEN range query on the sort key.
39
+ */
40
+ export declare const sequentialChunk: (cells: string[]) => string[][];
41
+ /**
42
+ * Compute H3 cell at the given resolution for coordinates.
43
+ */
44
+ export declare const computeCell: (latitude: number, longitude: number, resolution: number) => string;
45
+ /**
46
+ * Compute parent cell at the given resolution.
47
+ */
48
+ export declare const computeParentCell: (cell: string, resolution: number) => string;
49
+ /**
50
+ * Compute the hourly time partition string for a timestamp.
51
+ */
52
+ export declare const computeTimePartition: (timestampMs: number, bucketMs: number) => string;
53
+ /**
54
+ * Generate all time partition strings covering a time window.
55
+ */
56
+ export declare const getTimePartitions: (startMs: number, endMs: number, bucketMs: number) => string[];
57
+ /**
58
+ * Compute the precise child cell at resolution 15 for range query lower bounds.
59
+ */
60
+ export declare const cellToCenterChild: (cell: string, resolution: number) => string;
61
+ /**
62
+ * Compute upper bound cell string for BETWEEN queries.
63
+ * Adjusts the resolution hex digit to the target resolution.
64
+ */
65
+ export declare const cellToUpperBound: (cell: string, targetResolution: number) => string;
66
+ /**
67
+ * The bucket size in milliseconds for hourly partitions.
68
+ */
69
+ export declare const HOURLY_BUCKET_MS: number;
70
+ //# sourceMappingURL=H3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"H3.d.ts","sourceRoot":"","sources":["../src/H3.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,MAAM,OAAO,GAAG,GAAG,GAAG,IAAI,CAAA;AAShC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,EAAE,WAAW,MAAM,EAAE,QAAQ,MAAM,KAAG,MA0BvF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,MAAM,OAAO,KAAG,MAWtE,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,YAAY,MAAM,EAClB,YAAY,MAAM,KACjB,MAmCF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,QAAQ,MAAM,EACd,OAAO,MAAM,EAAE,EAAE,KAChB,MAAM,EAAE,EA0BV,CAAA;AAkBD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,EAAE,KAAG,MAAM,EAAE,EAoBzD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,WAAW,MAAM,EAAE,YAAY,MAAM,KAAG,MACpC,CAAA;AAElD;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAClC,CAAA;AAEnC;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,aAAa,MAAM,EAAE,UAAU,MAAM,KAAG,MACjC,CAAA;AAE5C;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,EAAE,OAAO,MAAM,EAAE,UAAU,MAAM,KAAG,MAAM,EAS1F,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAC7B,CAAA;AAExC;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,kBAAkB,MAAM,KAAG,MACR,CAAA;AAElE;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAU,CAAA"}
package/dist/H3.js ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * H3 — Pure H3 hexagonal grid algorithms for geospatial indexing.
3
+ *
4
+ * Ported from midgard's TelemetryRepository. All functions are pure
5
+ * (no Effect dependencies) and operate on H3 cells and coordinates.
6
+ */
7
+ import * as h3 from "h3-js";
8
+ import * as Spherical from "./Spherical.js";
9
+ // Pre-compute average edge lengths for each H3 resolution (0-15)
10
+ const AVERAGE_EDGE_LENGTHS = Object.fromEntries(Array.from({ length: 16 }, (_, i) => [i, h3.getHexagonEdgeLengthAvg(i, h3.UNITS.m)]));
11
+ const HOUR_MS = 1000 * 60 * 60;
12
+ /**
13
+ * Find the H3 resolution where hexagon edge length best matches the search radius.
14
+ *
15
+ * Starts with an estimate from average edge lengths, then refines using
16
+ * actual edge lengths at the given location (hexagons vary by latitude).
17
+ */
18
+ export const optimalResolution = (latitude, longitude, radius) => {
19
+ let startResolution = 0;
20
+ for (let i = 0; i < 15; i++) {
21
+ const current = AVERAGE_EDGE_LENGTHS[i];
22
+ const next = AVERAGE_EDGE_LENGTHS[i + 1];
23
+ if (current !== undefined && next !== undefined && current > radius && radius >= next) {
24
+ startResolution = i;
25
+ break;
26
+ }
27
+ }
28
+ for (let i = startResolution; i < 15; i++) {
29
+ const cell1 = h3.latLngToCell(latitude, longitude, i);
30
+ const edges1 = h3.originToDirectedEdges(cell1).map((edge) => h3.edgeLength(edge, h3.UNITS.m));
31
+ const minEdge1 = Math.min(...edges1);
32
+ const cell2 = h3.latLngToCell(latitude, longitude, i + 1);
33
+ const edges2 = h3.originToDirectedEdges(cell2).map((edge) => h3.edgeLength(edge, h3.UNITS.m));
34
+ const minEdge2 = Math.min(...edges2);
35
+ if (minEdge1 > radius && radius >= minEdge2) {
36
+ return i;
37
+ }
38
+ }
39
+ return startResolution;
40
+ };
41
+ /**
42
+ * Calculate the number of hexagonal rings (k) needed to cover a given radius
43
+ * from a center cell.
44
+ */
45
+ export const optimalK = (cell, radius, unit) => {
46
+ const edges = h3
47
+ .originToDirectedEdges(cell)
48
+ .map((edge) => h3.edgeLength(edge, unit === "km" ? h3.UNITS.km : h3.UNITS.m));
49
+ const minEdgeLength = Math.min(...edges);
50
+ const ratio = radius / minEdgeLength;
51
+ const hasLinearRatio = Math.ceil(ratio) % 2 !== 0 || ratio < 1;
52
+ return hasLinearRatio
53
+ ? Math.ceil((2 / 3) * ratio + 1 / 3)
54
+ : Math.ceil(1 / 3 + (1 / 3) * Math.sqrt(4 * ratio ** 2 - 3));
55
+ };
56
+ /**
57
+ * Compute the distance from a point to the edge between two neighboring cells.
58
+ *
59
+ * Uses spherical geometry to find the intersection of the bearing from
60
+ * the target cell center toward the search point with the shared edge boundary.
61
+ */
62
+ export const edgeDistance = (latitude, longitude, sourceCell, targetCell) => {
63
+ const targetCenter = h3.cellToLatLng(targetCell);
64
+ const targetCenterPoint = {
65
+ latitude: targetCenter[0],
66
+ longitude: targetCenter[1],
67
+ };
68
+ const locationPoint = { latitude, longitude };
69
+ const targetCenterBearing = Spherical.initialBearing(targetCenterPoint, locationPoint);
70
+ const edge = h3.cellsToDirectedEdge(sourceCell, targetCell);
71
+ const edgeLine = h3.directedEdgeToBoundary(edge);
72
+ const edgePointA = edgeLine[0];
73
+ const edgePointB = edgeLine[1];
74
+ if (!edgePointA || !edgePointB)
75
+ return Infinity;
76
+ const edgeA = { latitude: edgePointA[0], longitude: edgePointA[1] };
77
+ const edgeB = { latitude: edgePointB[0], longitude: edgePointB[1] };
78
+ const edgeBearing = Spherical.initialBearing(edgeA, edgeB);
79
+ const intersectionPoint = Spherical.intersection(targetCenterPoint, targetCenterBearing, edgeA, edgeBearing);
80
+ if (intersectionPoint === undefined)
81
+ return Infinity;
82
+ return h3.greatCircleDistance([latitude, longitude], [intersectionPoint.latitude, intersectionPoint.longitude], h3.UNITS.m);
83
+ };
84
+ /**
85
+ * Remove cells from outer rings that are beyond the search radius.
86
+ *
87
+ * Walks outward ring by ring, checking edge distance from the search center
88
+ * to each cell's shared boundary with its inner-ring neighbor.
89
+ */
90
+ export const pruneCells = (latitude, longitude, radius, cells) => {
91
+ const firstRing = cells[0];
92
+ if (!firstRing)
93
+ return [];
94
+ const prunedCells = [firstRing];
95
+ for (let i = 0; i < cells.length - 1; i++) {
96
+ const sourceCells = cells[i];
97
+ const targetCells = cells[i + 1];
98
+ if (!sourceCells || !targetCells)
99
+ continue;
100
+ const prunedSet = new Set();
101
+ for (const sourceCell of sourceCells) {
102
+ for (const targetCell of targetCells) {
103
+ if (!prunedSet.has(targetCell) && h3.areNeighborCells(sourceCell, targetCell)) {
104
+ const distance = edgeDistance(latitude, longitude, sourceCell, targetCell);
105
+ if (radius > distance) {
106
+ prunedSet.add(targetCell);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ prunedCells.push(Array.from(prunedSet).sort());
112
+ }
113
+ return prunedCells;
114
+ };
115
+ // Compute the difference between two H3 cell indexes, normalized by resolution
116
+ const cellDifference = (a, b) => {
117
+ return (BigInt(`0x${b}`) - BigInt(`0x${a}`)) >> ((BigInt(15) - BigInt(`0x${b[1]}`)) * BigInt(3));
118
+ };
119
+ // Extended cell difference accounting for parent cell boundaries
120
+ const extendedCellDifference = (a, b) => {
121
+ return (cellDifference(a, b) -
122
+ cellDifference(h3.cellToParent(a, h3.getResolution(a) - 1), h3.cellToParent(b, h3.getResolution(b) - 1)));
123
+ };
124
+ /**
125
+ * Group sorted H3 cells into chunks of contiguous cells.
126
+ *
127
+ * Contiguous cells (adjacent in the H3 index space) can be queried
128
+ * with a single BETWEEN range query on the sort key.
129
+ */
130
+ export const sequentialChunk = (cells) => {
131
+ const sorted = [...cells].sort();
132
+ const first = sorted[0];
133
+ if (!first)
134
+ return [];
135
+ const chunks = [[first]];
136
+ for (let i = 1; i < sorted.length; i++) {
137
+ const cell = sorted[i];
138
+ if (!cell)
139
+ continue;
140
+ const lastChunk = chunks[chunks.length - 1];
141
+ const lastCell = lastChunk?.[lastChunk.length - 1];
142
+ if (lastCell && extendedCellDifference(lastCell, cell) === BigInt(1)) {
143
+ lastChunk.push(cell);
144
+ }
145
+ else {
146
+ chunks.push([cell]);
147
+ }
148
+ }
149
+ return chunks;
150
+ };
151
+ /**
152
+ * Compute H3 cell at the given resolution for coordinates.
153
+ */
154
+ export const computeCell = (latitude, longitude, resolution) => h3.latLngToCell(latitude, longitude, resolution);
155
+ /**
156
+ * Compute parent cell at the given resolution.
157
+ */
158
+ export const computeParentCell = (cell, resolution) => h3.cellToParent(cell, resolution);
159
+ /**
160
+ * Compute the hourly time partition string for a timestamp.
161
+ */
162
+ export const computeTimePartition = (timestampMs, bucketMs) => String(Math.floor(timestampMs / bucketMs));
163
+ /**
164
+ * Generate all time partition strings covering a time window.
165
+ */
166
+ export const getTimePartitions = (startMs, endMs, bucketMs) => {
167
+ const partitions = new Set();
168
+ let time = startMs;
169
+ while (time <= endMs) {
170
+ partitions.add(computeTimePartition(time, bucketMs));
171
+ time += bucketMs;
172
+ }
173
+ partitions.add(computeTimePartition(endMs, bucketMs));
174
+ return Array.from(partitions);
175
+ };
176
+ /**
177
+ * Compute the precise child cell at resolution 15 for range query lower bounds.
178
+ */
179
+ export const cellToCenterChild = (cell, resolution) => h3.cellToCenterChild(cell, resolution);
180
+ /**
181
+ * Compute upper bound cell string for BETWEEN queries.
182
+ * Adjusts the resolution hex digit to the target resolution.
183
+ */
184
+ export const cellToUpperBound = (cell, targetResolution) => cell.slice(0, 1) + targetResolution.toString(16) + cell.slice(2);
185
+ /**
186
+ * The bucket size in milliseconds for hourly partitions.
187
+ */
188
+ export const HOURLY_BUCKET_MS = HOUR_MS;
189
+ //# sourceMappingURL=H3.js.map
package/dist/H3.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"H3.js","sourceRoot":"","sources":["../src/H3.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Spherical — Pure spherical geometry functions.
3
+ *
4
+ * Provides:
5
+ * - `initialBearingTo` — great-circle initial bearing (forward azimuth)
6
+ * - `intersection` — intersection of two great-circle paths
7
+ *
8
+ * All angles are in degrees externally, radians internally.
9
+ */
10
+ export interface LatLng {
11
+ readonly latitude: number;
12
+ readonly longitude: number;
13
+ }
14
+ /**
15
+ * Compute the initial bearing (forward azimuth) from `from` to `to`
16
+ * along a great-circle path.
17
+ *
18
+ * Formula:
19
+ * θ = atan2(sin(Δλ)·cos(φ₂), cos(φ₁)·sin(φ₂) − sin(φ₁)·cos(φ₂)·cos(Δλ))
20
+ *
21
+ * Returns bearing in degrees [0, 360).
22
+ */
23
+ export declare const initialBearing: (from: LatLng, to: LatLng) => number;
24
+ /**
25
+ * Find the intersection point of two great-circle paths defined by
26
+ * a start point and initial bearing each.
27
+ *
28
+ * Uses the Vincenty formula to compute the intersection.
29
+ * Returns `undefined` when paths are parallel, antipodal, or ambiguous.
30
+ */
31
+ export declare const intersection: (p1: LatLng, brng1: number, p2: LatLng, brng2: number) => LatLng | undefined;
32
+ /**
33
+ * Compute the great-circle distance between two points in meters.
34
+ * Uses the Haversine formula.
35
+ */
36
+ export declare const greatCircleDistance: (from: LatLng, to: LatLng) => number;
37
+ //# sourceMappingURL=Spherical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Spherical.d.ts","sourceRoot":"","sources":["../src/Spherical.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAKD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,IAAI,MAAM,KAAG,MAUzD,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GACvB,IAAI,MAAM,EACV,OAAO,MAAM,EACb,IAAI,MAAM,EACV,OAAO,MAAM,KACZ,MAAM,GAAG,SA2DX,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,EAAE,IAAI,MAAM,KAAG,MAW9D,CAAA"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Spherical — Pure spherical geometry functions.
3
+ *
4
+ * Provides:
5
+ * - `initialBearingTo` — great-circle initial bearing (forward azimuth)
6
+ * - `intersection` — intersection of two great-circle paths
7
+ *
8
+ * All angles are in degrees externally, radians internally.
9
+ */
10
+ const toRad = (deg) => (deg * Math.PI) / 180;
11
+ const toDeg = (rad) => (rad * 180) / Math.PI;
12
+ /**
13
+ * Compute the initial bearing (forward azimuth) from `from` to `to`
14
+ * along a great-circle path.
15
+ *
16
+ * Formula:
17
+ * θ = atan2(sin(Δλ)·cos(φ₂), cos(φ₁)·sin(φ₂) − sin(φ₁)·cos(φ₂)·cos(Δλ))
18
+ *
19
+ * Returns bearing in degrees [0, 360).
20
+ */
21
+ export const initialBearing = (from, to) => {
22
+ const φ1 = toRad(from.latitude);
23
+ const φ2 = toRad(to.latitude);
24
+ const Δλ = toRad(to.longitude - from.longitude);
25
+ const x = Math.sin(Δλ) * Math.cos(φ2);
26
+ const y = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
27
+ const θ = Math.atan2(x, y);
28
+ return (toDeg(θ) + 360) % 360;
29
+ };
30
+ /**
31
+ * Find the intersection point of two great-circle paths defined by
32
+ * a start point and initial bearing each.
33
+ *
34
+ * Uses the Vincenty formula to compute the intersection.
35
+ * Returns `undefined` when paths are parallel, antipodal, or ambiguous.
36
+ */
37
+ export const intersection = (p1, brng1, p2, brng2) => {
38
+ const φ1 = toRad(p1.latitude);
39
+ const λ1 = toRad(p1.longitude);
40
+ const φ2 = toRad(p2.latitude);
41
+ const λ2 = toRad(p2.longitude);
42
+ const θ13 = toRad(brng1);
43
+ const θ23 = toRad(brng2);
44
+ const Δφ = φ2 - φ1;
45
+ const Δλ = λ2 - λ1;
46
+ // Angular distance p1-p2
47
+ const δ12 = 2 *
48
+ Math.asin(Math.sqrt(Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2));
49
+ if (Math.abs(δ12) < Number.EPSILON)
50
+ return undefined;
51
+ // Initial/final bearings between p1-p2
52
+ const cosθa = (Math.sin(φ2) - Math.sin(φ1) * Math.cos(δ12)) / (Math.sin(δ12) * Math.cos(φ1));
53
+ const cosθb = (Math.sin(φ1) - Math.sin(φ2) * Math.cos(δ12)) / (Math.sin(δ12) * Math.cos(φ2));
54
+ const θa = Math.acos(Math.min(1, Math.max(-1, cosθa)));
55
+ const θb = Math.acos(Math.min(1, Math.max(-1, cosθb)));
56
+ const θ12 = Math.sin(λ2 - λ1) > 0 ? θa : 2 * Math.PI - θa;
57
+ const θ21 = Math.sin(λ2 - λ1) > 0 ? 2 * Math.PI - θb : θb;
58
+ const α1 = θ13 - θ12;
59
+ const α2 = θ21 - θ23;
60
+ // Check for parallel/antipodal paths
61
+ if (Math.sin(α1) === 0 && Math.sin(α2) === 0)
62
+ return undefined;
63
+ if (Math.sin(α1) * Math.sin(α2) < 0)
64
+ return undefined;
65
+ const cosα3 = -Math.cos(α1) * Math.cos(α2) + Math.sin(α1) * Math.sin(α2) * Math.cos(δ12);
66
+ const δ13 = Math.atan2(Math.sin(δ12) * Math.sin(α1) * Math.sin(α2), Math.cos(α2) + Math.cos(α1) * cosα3);
67
+ const φ3 = Math.asin(Math.min(1, Math.max(-1, Math.sin(φ1) * Math.cos(δ13) + Math.cos(φ1) * Math.sin(δ13) * Math.cos(θ13))));
68
+ const Δλ13 = Math.atan2(Math.sin(θ13) * Math.sin(δ13) * Math.cos(φ1), Math.cos(δ13) - Math.sin(φ1) * Math.sin(φ3));
69
+ const λ3 = λ1 + Δλ13;
70
+ return {
71
+ latitude: toDeg(φ3),
72
+ longitude: ((toDeg(λ3) + 540) % 360) - 180,
73
+ };
74
+ };
75
+ /**
76
+ * Compute the great-circle distance between two points in meters.
77
+ * Uses the Haversine formula.
78
+ */
79
+ export const greatCircleDistance = (from, to) => {
80
+ const R = 6371008.8; // Earth mean radius in meters
81
+ const φ1 = toRad(from.latitude);
82
+ const φ2 = toRad(to.latitude);
83
+ const Δφ = toRad(to.latitude - from.latitude);
84
+ const Δλ = toRad(to.longitude - from.longitude);
85
+ const a = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2;
86
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
87
+ return R * c;
88
+ };
89
+ //# sourceMappingURL=Spherical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Spherical.js","sourceRoot":"","sources":["../src/Spherical.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export type { BoundGeoIndex, Coordinates, GeoEntity, GeoFields, GeoIndex as GeoIndexType, NearbyOptions, NearbyResult, SortOrder, TimeBucket, } from "./GeoIndex.js";
2
+ export * as GeoIndex from "./GeoIndex.js";
3
+ export * as H3 from "./H3.js";
4
+ export type { LatLng } from "./Spherical.js";
5
+ export * as Spherical from "./Spherical.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,WAAW,EACX,SAAS,EACT,SAAS,EACT,QAAQ,IAAI,YAAY,EACxB,aAAa,EACb,YAAY,EACZ,SAAS,EACT,UAAU,GACX,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * as GeoIndex from "./GeoIndex.js";
2
+ export * as H3 from "./H3.js";
3
+ export * as Spherical from "./Spherical.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAE7B,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@effect-dynamodb/geo",
3
+ "version": "0.1.0",
4
+ "description": "Geospatial index and search for effect-dynamodb using H3 hexagonal grid",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Justin Menga",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/jmenga/effect-dynamodb",
11
+ "directory": "packages/effect-dynamodb-geo"
12
+ },
13
+ "homepage": "https://jmenga.github.io/effect-dynamodb/guides/geospatial",
14
+ "bugs": {
15
+ "url": "https://github.com/jmenga/effect-dynamodb/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "keywords": [
21
+ "effect",
22
+ "effect-ts",
23
+ "dynamodb",
24
+ "geospatial",
25
+ "h3",
26
+ "hexagonal-grid",
27
+ "geo-search",
28
+ "location",
29
+ "proximity"
30
+ ],
31
+ "sideEffects": false,
32
+ "main": "dist/index.js",
33
+ "types": "dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "test": "vitest run",
48
+ "test:watch": "vitest",
49
+ "test:connected": "vitest run --config vitest.connected.ts",
50
+ "check": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json",
51
+ "lint": "biome check .",
52
+ "lint:fix": "biome check --write ."
53
+ },
54
+ "dependencies": {
55
+ "h3-js": "^4.2.1"
56
+ },
57
+ "peerDependencies": {
58
+ "effect": "^4.0.0-beta.48",
59
+ "effect-dynamodb": "workspace:^"
60
+ },
61
+ "devDependencies": {
62
+ "@aws-sdk/client-dynamodb": "^3.700.0",
63
+ "@effect/vitest": "4.0.0-beta.48",
64
+ "@types/node": "^25.0.0",
65
+ "effect": "4.0.0-beta.48",
66
+ "effect-dynamodb": "workspace:*",
67
+ "typescript": "^5.7.0",
68
+ "vitest": "^4.1.0"
69
+ }
70
+ }