From 1f0cc9933b1960e01c0f8ff8419039859db420ab Mon Sep 17 00:00:00 2001 From: Brandon Keepers Date: Mon, 23 Feb 2026 09:11:35 -0500 Subject: [PATCH] Add bbox search --- README.md | 29 +++++++++++++++++++++++++++++ src/search/index.ts | 21 +++++++++++++++++++-- test/search.test.ts | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c7f3c4d4a..c46cfb35e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,35 @@ Both functions take the following parameters: - `maxResults`: Maximum number of results to return (default: `10`). +##### Bounding box search + +You can find all stations within a geographic bounding box using the `bbox` function: + +```typescript +import { bbox } from "@neaps/tide-database"; + +// Find stations in the Boston area +const stations = bbox(-71.5, 42, -70.5, 42.8); +console.log("Stations in bounds:", stations.length); + +// With a filter +const referenceOnly = bbox( + -72, + 41, + -70, + 43, + (station) => station.type === "reference", +); +``` + +Parameters: + +- `minLon`: Minimum longitude (west edge of the bounding box). +- `minLat`: Minimum latitude (south edge of the bounding box). +- `maxLon`: Maximum longitude (east edge of the bounding box). +- `maxLat`: Maximum latitude (north edge of the bounding box). +- `filter`: Optional function that takes a station and returns `true` to include it in results, or `false` to exclude it. + ##### Full-text search You can search for stations by name, region, country, or continent using the `search` function. It supports fuzzy matching and prefix search: diff --git a/src/search/index.ts b/src/search/index.ts index be7e2d2bf..c82e7ad55 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -10,9 +10,11 @@ export type Position = Latitude & Longitude; type Latitude = { latitude: number } | { lat: number }; type Longitude = { longitude: number } | { lon: number } | { lng: number }; +export type Filter = (station: Station) => boolean; + export type NearestOptions = Position & { maxDistance?: number; - filter?: (station: Station) => boolean; + filter?: Filter; }; export type NearOptions = NearestOptions & { @@ -20,7 +22,7 @@ export type NearOptions = NearestOptions & { }; export type TextSearchOptions = { - filter?: (station: Station) => boolean; + filter?: Filter; maxResults?: number; }; @@ -66,6 +68,21 @@ export function nearest(options: NearestOptions): StationWithDistance | null { return results[0] ?? null; } +/** + * Find stations within a bounding box. + */ +export function bbox( + minLon: number, + minLat: number, + maxLon: number, + maxLat: number, + filter?: Filter, +): Station[] { + const ids: number[] = geoIndex.range(minLon, minLat, maxLon, maxLat); + const results = ids.map((id) => stations[id]!); + return filter ? results.filter(filter) : results; +} + export function positionToPoint(options: Position): [number, number] { const longitude = "longitude" in options diff --git a/test/search.test.ts b/test/search.test.ts index 0c9fd7dd2..e626d9bdd 100644 --- a/test/search.test.ts +++ b/test/search.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { near, nearest, search } from "../src/index.js"; +import { near, nearest, search, bbox } from "../src/index.js"; describe("near", () => { [ @@ -68,6 +68,40 @@ describe("nearest", () => { }); }); +describe("bbox", () => { + test("returns stations within bounding box", () => { + // Boston area: roughly -71.5 to -70.5 lon, 42 to 42.8 lat + const results = bbox(-71.5, 42, -70.5, 42.8); + expect(results.length).toBeGreaterThan(0); + results.forEach((station) => { + expect(station.longitude).toBeGreaterThanOrEqual(-71.5); + expect(station.longitude).toBeLessThanOrEqual(-70.5); + expect(station.latitude).toBeGreaterThanOrEqual(42); + expect(station.latitude).toBeLessThanOrEqual(42.8); + }); + }); + + test("returns empty array for bbox with no stations", () => { + // Middle of the Pacific + const results = bbox(-170, -50, -169, -49); + expect(results).toEqual([]); + }); + + test("can filter results", () => { + const results = bbox( + -72, + 41, + -70, + 43, + (station) => station.type === "reference", + ); + expect(results.length).toBeGreaterThan(0); + results.forEach((station) => { + expect(station.type).toBe("reference"); + }); + }); +}); + describe("search", () => { test("searches by name", () => { const results = search("Boston");