@squawk/airspace 0.8.6 → 0.9.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/README.md CHANGED
@@ -3,8 +3,9 @@
3
3
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../LICENSE.md) [![npm](https://img.shields.io/npm/v/@squawk/airspace)](https://www.npmjs.com/package/@squawk/airspace) ![TypeScript](https://img.shields.io/badge/TypeScript-blue?logo=typescript&logoColor=white)
4
4
 
5
5
  Pure logic library for querying US airspace geometry. Given a position and altitude,
6
- returns all applicable airspace designations. Contains no bundled data - accepts a
7
- GeoJSON dataset at initialization. For zero-config use, pair with `@squawk/airspace-data`.
6
+ returns all applicable airspace designations, or look up and fuzzy-search features by
7
+ identifier and name. Contains no bundled data - accepts a GeoJSON dataset at
8
+ initialization. For zero-config use, pair with `@squawk/airspace-data`.
8
9
 
9
10
  Part of the [@squawk](https://www.npmjs.com/org/squawk) aviation library suite. See all packages on npm.
10
11
 
@@ -40,6 +41,10 @@ const laxShells = resolver.byAirport('LAX');
40
41
 
41
42
  // Get every ARTCC center boundary (one feature per stratum)
42
43
  const ny = resolver.byArtcc('ZNY');
44
+
45
+ // Fuzzy-search features by identifier and name (scored, best match first)
46
+ const matches = resolver.search({ text: 'LAX' });
47
+ console.log(matches[0]?.feature.identifier, matches[0]?.score);
43
48
  ```
44
49
 
45
50
  Consumers who have their own GeoJSON airspace data can use this package standalone:
@@ -82,6 +87,9 @@ returns a resolver object with the following methods:
82
87
  - `byIdentifier(identifier, options?)` - type-agnostic identifier lookup that
83
88
  spans both partitions in one call, with an optional `types` inclusion
84
89
  filter and `includeArtcc` toggle.
90
+ - `search(AirspaceSearchQuery)` - fuzzy-searches features by identifier and
91
+ name, returning scored results best-match first with the matched field and
92
+ highlight ranges, optionally filtered by type.
85
93
  - `byCentroid({ lon, lat, toleranceDeg? })` - returns features whose polygon
86
94
  centroid lies within tolerance of the query coordinates. Useful for
87
95
  resolving features whose `identifier` is empty (some Class E5 surfaces),
@@ -130,8 +138,8 @@ Creates a resolver from a GeoJSON dataset.
130
138
 
131
139
  **Returns:** `AirspaceResolver` - an object exposing `query(AirspaceQuery)`,
132
140
  `byAirport(identifier, types?)`, `byArtcc(identifier, stratum?)`,
133
- `byIdentifier(identifier, options?)`, `byCentroid(query)`, `withinBbox(bbox)`,
134
- and `forEachIndexed(callback)` methods.
141
+ `byIdentifier(identifier, options?)`, `search(query)`, `byCentroid(query)`,
142
+ `withinBbox(bbox)`, and `forEachIndexed(callback)` methods.
135
143
 
136
144
  ### `AirspaceQuery`
137
145
 
@@ -259,6 +267,36 @@ resolver.forEachIndexed((feature, ring, boundingBox) => {
259
267
  });
260
268
  ```
261
269
 
270
+ ### `resolver.search(query)`
271
+
272
+ Fuzzy-searches airspace features across identifier and name. Matching is
273
+ case-insensitive and tolerant of prefixes, substrings, subsequences, and small
274
+ typos. Results are scored and returned best-match first.
275
+
276
+ | Property | Type | Description |
277
+ | ---------- | -------------------------- | ---------------------------------------------------------------------------------------- |
278
+ | `text` | string | Search text, matched fuzzily against each feature's identifier and name |
279
+ | `limit` | number | Optional. Maximum number of results. Defaults to 20 |
280
+ | `types` | ReadonlySet\<AirspaceType> | Optional. When provided, only features of these types are returned |
281
+ | `minScore` | number | Optional. Minimum match score (exclusive) in `[0, 1]` a result must reach. Defaults to 0 |
282
+
283
+ Returns `AirspaceSearchResult[]`, sorted by descending score, each containing:
284
+
285
+ - `feature` - the matched `AirspaceFeature` record
286
+ - `score` - match strength in `[0, 1]`, where 1 is an exact identifier or name match
287
+ - `matchedField` - which field produced the best match: `'identifier'` or `'name'`
288
+ - `ranges` - matched character ranges within the best-matching field's text, for highlighting
289
+
290
+ Features with an empty identifier and name (some Class E5 surfaces) never match a
291
+ non-empty query and are simply absent from results.
292
+
293
+ ```typescript
294
+ const results = resolver.search({ text: 'LAX', limit: 10 });
295
+ for (const { feature, score, matchedField } of results) {
296
+ console.log(feature.identifier, score, `(matched ${matchedField})`);
297
+ }
298
+ ```
299
+
262
300
  ### ARTCC altitude bounds
263
301
 
264
302
  ARTCC features carry stratum-aligned floor/ceiling values for use with
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * @packageDocumentation
3
3
  * Pure logic library for point-in-airspace queries against US airspace polygons.
4
4
  */
5
+ export type { MatchRange } from '@squawk/search';
5
6
  export { createAirspaceResolver } from './resolver.js';
6
- export type { AirspaceResolver, AirspaceResolverOptions, AirspaceQuery, AirspaceCentroidQuery, AirspaceByIdentifierOptions, } from './resolver.js';
7
+ export type { AirspaceResolver, AirspaceResolverOptions, AirspaceQuery, AirspaceCentroidQuery, AirspaceByIdentifierOptions, AirspaceSearchField, AirspaceSearchQuery, AirspaceSearchResult, } from './resolver.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,2BAA2B,GAC5B,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1 @@
1
- /**
2
- * @packageDocumentation
3
- * Pure logic library for point-in-airspace queries against US airspace polygons.
4
- */
5
1
  export { createAirspaceResolver } from './resolver.js';
@@ -1,5 +1,6 @@
1
1
  import type { FeatureCollection } from 'geojson';
2
2
  import { type BoundingBox } from '@squawk/geo';
3
+ import type { MatchRange } from '@squawk/search';
3
4
  import type { AirspaceFeature, AirspaceType, ArtccStratum } from '@squawk/types';
4
5
  /**
5
6
  * A query describing a geographic position and altitude to resolve
@@ -64,6 +65,36 @@ export interface AirspaceByIdentifierOptions {
64
65
  */
65
66
  includeArtcc?: boolean;
66
67
  }
68
+ /**
69
+ * The searchable fields an airspace {@link AirspaceSearchResult} can match on.
70
+ */
71
+ export type AirspaceSearchField = 'identifier' | 'name';
72
+ /**
73
+ * Options for a fuzzy text search query against airspace identifiers and names.
74
+ */
75
+ export interface AirspaceSearchQuery {
76
+ /** Search text, matched fuzzily and case-insensitively against each feature's identifier and name. */
77
+ text: string;
78
+ /** Maximum number of results to return. Defaults to 20. */
79
+ limit?: number;
80
+ /** Optional set of airspace types to include. When omitted, all types are included. */
81
+ types?: ReadonlySet<AirspaceType>;
82
+ /** Minimum match score (exclusive) in `[0, 1]` a result must reach. Defaults to 0, which keeps every match. Raise it to drop weak fuzzy matches. */
83
+ minScore?: number;
84
+ }
85
+ /**
86
+ * A scored airspace result from a fuzzy {@link AirspaceResolver.search}.
87
+ */
88
+ export interface AirspaceSearchResult {
89
+ /** The matched airspace feature. */
90
+ feature: AirspaceFeature;
91
+ /** Match strength in `[0, 1]`, where 1 is an exact identifier or name match. */
92
+ score: number;
93
+ /** Which field produced the best match, identifying what {@link AirspaceSearchResult.ranges} index into. */
94
+ matchedField: AirspaceSearchField;
95
+ /** Matched character ranges within the best-matching field's text, for highlighting. */
96
+ ranges: MatchRange[];
97
+ }
67
98
  /**
68
99
  * Options for creating an airspace resolver.
69
100
  */
@@ -159,6 +190,15 @@ export interface AirspaceResolver {
159
190
  * @returns All matching features, or an empty array.
160
191
  */
161
192
  byIdentifier(identifier: string, options?: AirspaceByIdentifierOptions): AirspaceFeature[];
193
+ /**
194
+ * Fuzzy-searches airspace features across identifier and name. Results are
195
+ * scored and returned best-match first, each carrying the matched field and
196
+ * character ranges for highlighting.
197
+ *
198
+ * Features with an empty identifier and name (some Class E5 surfaces) never
199
+ * match a non-empty query and are simply absent from results.
200
+ */
201
+ search(query: AirspaceSearchQuery): AirspaceSearchResult[];
162
202
  /**
163
203
  * Returns every airspace feature whose pre-indexed bounding box overlaps
164
204
  * the given bounding box. Reuses the bounding box computed once at
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAW,MAAM,SAAS,CAAC;AAE1D,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAiB,YAAY,EAAE,MAAM,eAAe,CAAC;AAIhG;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CACnC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAClC;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,8DAA8D;IAC9D,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,EAAE,CAAC;IAE/C;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,eAAe,EAAE,CAAC;IAEpF;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,eAAe,EAAE,CAAC;IAEvE;;;;;;;;;;;;;;;;;OAiBG;IACH,UAAU,CAAC,KAAK,EAAE,qBAAqB,GAAG,eAAe,EAAE,CAAC;IAE5D;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,2BAA2B,GAAG,eAAe,EAAE,CAAC;IAE3F;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,EAAE,CAAC;IAEjD;;;;;;;;;;;;OAYG;IACH,cAAc,CACZ,QAAQ,EAAE,CACR,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EACzB,WAAW,EAAE,WAAW,KACrB,IAAI,GACR,IAAI,CAAC;CACT;AAoDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA2HzF"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"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"}
package/dist/resolver.js CHANGED
@@ -1,5 +1,10 @@
1
1
  import { polygon, polygonGeoJson } from '@squawk/geo';
2
+ import { fuzzySearch } from '@squawk/search';
2
3
  import { altitudeMatches } from './vertical-filter.js';
4
+ /**
5
+ * Default maximum number of results for text search queries.
6
+ */
7
+ const DEFAULT_SEARCH_LIMIT = 20;
3
8
  /**
4
9
  * Parses a GeoJSON Feature into an IndexedFeature, extracting the
5
10
  * AirspaceFeature properties and polygon ring. Returns null if the
@@ -81,6 +86,7 @@ export function createAirspaceResolver(options) {
81
86
  }
82
87
  }
83
88
  }
89
+ const searchableFeatures = indexed.map((entry) => entry.feature);
84
90
  return {
85
91
  query(query) {
86
92
  const results = [];
@@ -153,6 +159,26 @@ export function createAirspaceResolver(options) {
153
159
  }
154
160
  return bucket.filter((f) => f.type !== 'ARTCC');
155
161
  },
162
+ search(query) {
163
+ const options = {
164
+ keys: (feature) => [
165
+ { name: 'identifier', text: feature.identifier },
166
+ { name: 'name', text: feature.name },
167
+ ],
168
+ limit: query.limit ?? DEFAULT_SEARCH_LIMIT,
169
+ minScore: query.minScore ?? 0,
170
+ };
171
+ const types = query.types;
172
+ if (types) {
173
+ options.filter = (feature) => types.has(feature.type);
174
+ }
175
+ return fuzzySearch(searchableFeatures, query.text, options).map((match) => ({
176
+ feature: match.item,
177
+ score: match.score,
178
+ matchedField: match.field,
179
+ ranges: match.ranges,
180
+ }));
181
+ },
156
182
  withinBbox(bbox) {
157
183
  const results = [];
158
184
  for (const { feature, boundingBox } of indexed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squawk/airspace",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Pure logic library for querying US airspace geometry by position and altitude",
6
6
  "author": "Neil Cochran",
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@squawk/geo": "^0.4.3",
45
+ "@squawk/search": "^0.1.0",
45
46
  "@squawk/types": "^0.8.0",
46
47
  "@types/geojson": "^7946.0.16"
47
48
  },