commit db239fc4efac2d1911de429466fe3442cf66b737
parent bad0798d404bba40617a5407a36c799b4dd711d7
Author: triesap <triesap@radroots.dev>
Date: Sat, 27 Dec 2025 18:40:46 +0000
geocoder: support configurable database and connect options
- Add GeocoderConfig/GeocoderConnectConfig and update IGeocoder.connect signature
- Normalize connect config and allow string shorthand for wasm_path
- Resolve database path via route-aware helper and default geonames location
- Export database path utilities/constants from geocoder entrypoints
Diffstat:
5 files changed, 110 insertions(+), 19 deletions(-)
diff --git a/geocoder/src/geocoder.ts b/geocoder/src/geocoder.ts
@@ -1,29 +1,38 @@
import type { GeolocationPoint } from "@radroots/geo";
import { err_msg, resolve_wasm_path } from "@radroots/utils";
import type { Database } from "sql.js";
-import type { GeocoderReverseResult, IGeocoder, IGeocoderConnectResolve, IGeocoderCountryCenter, IGeocoderCountryCenterResolve, IGeocoderCountryListResolve, IGeocoderCountryListResult, IGeocoderCountryResolve, IGeocoderReverseOpts, IGeocoderReverseResolve } from "./types.js";
-import { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result } from "./utils.js";
+import type { GeocoderConfig, GeocoderConnectConfig, GeocoderReverseResult, IGeocoder, IGeocoderConnectResolve, IGeocoderCountryCenter, IGeocoderCountryCenterResolve, IGeocoderCountryListResolve, IGeocoderCountryListResult, IGeocoderCountryResolve, IGeocoderReverseOpts, IGeocoderReverseResolve } from "./types.js";
+import { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result, resolve_geocoder_database_path } from "./utils.js";
const KM_PER_DEGREE_LATITUDE = 111;
const DEFAULT_SQL_WASM_PATH = `/assets/sql-wasm.wasm`;
-const DEFAULT_SQL_DATABASE_PATH = `/assets/geonames.db`;
+
+const normalize_geocoder_connect_config = (config?: GeocoderConnectConfig | string): GeocoderConnectConfig => {
+ if (!config) return {};
+ if (typeof config === `string`) return { wasm_path: config };
+ return config;
+};
export class Geocoder implements IGeocoder {
private _db: Database | null = null;
- private _database_name: string;
+ private _database_path: string;
- constructor(database_name?: string) {
- if (database_name && database_name.charAt(0) !== `/`) throw new Error(`Error: database name must be a valid path`);
- this._database_name = database_name || DEFAULT_SQL_DATABASE_PATH;
+ constructor(config?: GeocoderConfig | string) {
+ const database_path = typeof config === `string` ? config : config?.database_path;
+ this._database_path = resolve_geocoder_database_path(database_path);
}
- public async connect(wasm_path?: string): Promise<IGeocoderConnectResolve> {
+ public async connect(config?: GeocoderConnectConfig | string): Promise<IGeocoderConnectResolve> {
try {
+ const connect_config = normalize_geocoder_connect_config(config);
+ const database_path = connect_config.database_path
+ ? resolve_geocoder_database_path(connect_config.database_path)
+ : this._database_path;
const init_sqljs = await import(`sql.js`);
const sql = await init_sqljs.default({
- locateFile: wasm_file => resolve_wasm_path(wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH)
+ locateFile: wasm_file => resolve_wasm_path(connect_config.wasm_path, wasm_file, DEFAULT_SQL_WASM_PATH)
});
- const database_res = await fetch(this._database_name);
+ const database_res = await fetch(database_path);
const database_buffer = await database_res.arrayBuffer();
this._db = new sql.Database(new Uint8Array(database_buffer));
return true;
diff --git a/geocoder/src/index.ts b/geocoder/src/index.ts
@@ -1,4 +1,3 @@
export { Geocoder } from "./geocoder.js"
-export type { GeocoderDegreeOffset, GeocoderIError, GeocoderReverseResult, IGeocoder, IGeocoderCountryCenter, IGeocoderCountryListResult, IGeocoderReverseOpts } from "./types.js"
-export { parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result } from "./utils.js"
-
+export type { GeocoderConfig, GeocoderConnectConfig, GeocoderDegreeOffset, GeocoderIError, GeocoderReverseResult, IGeocoder, IGeocoderCountryCenter, IGeocoderCountryListResult, IGeocoderReverseOpts } from "./types.js"
+export { DEFAULT_GEOCODER_DATABASE_FILE, DEFAULT_GEOCODER_DATABASE_PATH, parse_geocode_country_center_result, parse_geocode_country_list_result, parse_geocode_reverse_result, parse_geocoder_database_route, resolve_geocoder_database_path, type GeocoderDatabaseRoute } from "./utils.js"
diff --git a/geocoder/src/types.ts b/geocoder/src/types.ts
@@ -21,6 +21,14 @@ export type GeocoderReverseResult = {
export type GeocoderDegreeOffset = 0.5 | 1.0 | 1.5 | 2.0 | 2.5 | 3
+export type GeocoderConfig = {
+ database_path?: string;
+};
+
+export type GeocoderConnectConfig = GeocoderConfig & {
+ wasm_path?: string;
+};
+
export type IGeocoderReverseOpts = {
degree_offset?: GeocoderDegreeOffset;
limit?: number | false;
@@ -39,7 +47,7 @@ export type IGeocoderCountryListResolve = ResultsList<IGeocoderCountryListResult
export type IGeocoderCountryCenterResolve = ResultObj<GeolocationPoint> | IError<GeocoderIError>;
export type IGeocoder = {
- connect(wasm_path?: string): Promise<IGeocoderConnectResolve>;
+ connect(config?: GeocoderConnectConfig | string): Promise<IGeocoderConnectResolve>;
reverse(point: GeolocationPoint, opts?: IGeocoderReverseOpts): Promise<IGeocoderReverseResolve>;
country(opts: IGeocoderCountryCenter): Promise<IGeocoderCountryResolve>;
country_list(): Promise<IGeocoderCountryListResolve>;
diff --git a/geocoder/src/utils.ts b/geocoder/src/utils.ts
@@ -1,6 +1,41 @@
import type { GeolocationPoint } from "@radroots/geo";
+import { parse_route_path, resolve_route_path } from "@radroots/utils";
import type { GeocoderReverseResult, IGeocoderCountryListResult } from "./types.js";
+export const DEFAULT_GEOCODER_DATABASE_FILE = `geonames.db`;
+export const DEFAULT_GEOCODER_DATABASE_PATH = `/geonames/geonames.db`;
+const GEOCODER_DATABASE_FILE_EXTS = [`.db`];
+
+export type GeocoderDatabaseRoute = {
+ base_path: string;
+ file_name: string | null;
+ query: string;
+ hash: string;
+};
+
+const is_geocoder_database_file = (segment: string): boolean => {
+ const lower_segment = segment.toLowerCase();
+ return GEOCODER_DATABASE_FILE_EXTS.some((ext) => lower_segment.endsWith(ext));
+};
+
+export const parse_geocoder_database_route = (database_path: string): GeocoderDatabaseRoute => {
+ const { path, query, hash } = parse_route_path(database_path);
+ const trimmed_path = path.endsWith(`/`) ? path.slice(0, -1) : path;
+ const last_slash = trimmed_path.lastIndexOf(`/`);
+ const last_segment = last_slash >= 0 ? trimmed_path.slice(last_slash + 1) : trimmed_path;
+ const file_name = is_geocoder_database_file(last_segment) ? last_segment : null;
+ const base_path = file_name && last_slash >= 0 ? trimmed_path.slice(0, last_slash) : trimmed_path;
+ return { base_path, file_name, query, hash };
+};
+
+export const resolve_geocoder_database_path = (database_path?: string): string =>
+ resolve_route_path(
+ database_path,
+ DEFAULT_GEOCODER_DATABASE_FILE,
+ DEFAULT_GEOCODER_DATABASE_PATH,
+ GEOCODER_DATABASE_FILE_EXTS
+ );
+
export const parse_geocode_reverse_result = (obj: any): GeocoderReverseResult | undefined => {
if (typeof obj !== `object` || !obj) return undefined;
const { id, name, admin1_id, admin1_name, country_id, country_name, latitude, longitude } = obj;
diff --git a/utils/src/path/index.ts b/utils/src/path/index.ts
@@ -1,6 +1,46 @@
-export const resolve_wasm_path = (wasm_path: string | undefined, wasm_file: string, default_wasm_path: string): string => {
- const resolved_wasm_path = wasm_path ?? default_wasm_path;
- if (resolved_wasm_path.endsWith(".wasm")) return resolved_wasm_path;
- const base_path = resolved_wasm_path.endsWith("/") ? resolved_wasm_path.slice(0, -1) : resolved_wasm_path;
- return `${base_path}/${wasm_file}`;
+export type RoutePathParts = {
+ path: string;
+ query: string;
+ hash: string;
};
+
+export const parse_route_path = (route_path: string): RoutePathParts => {
+ const query_idx = route_path.indexOf("?");
+ const hash_idx = route_path.indexOf("#");
+ let path_end = route_path.length;
+ if (query_idx >= 0 && hash_idx >= 0) path_end = Math.min(query_idx, hash_idx);
+ else if (query_idx >= 0) path_end = query_idx;
+ else if (hash_idx >= 0) path_end = hash_idx;
+ const path = route_path.slice(0, path_end);
+ const query = query_idx >= 0
+ ? route_path.slice(query_idx, hash_idx >= 0 ? hash_idx : undefined)
+ : "";
+ const hash = hash_idx >= 0 ? route_path.slice(hash_idx) : "";
+ return { path, query, hash };
+};
+
+const has_file_extension = (route_path: string, file_exts: readonly string[]): boolean => {
+ const lower_path = route_path.toLowerCase();
+ return file_exts.some((ext) => lower_path.endsWith(ext));
+};
+
+export const resolve_route_path = (
+ route_path: string | undefined,
+ file_name: string,
+ default_route_path: string,
+ file_exts: readonly string[]
+): string => {
+ const resolved_route_path = route_path ?? default_route_path;
+ const { path, query, hash } = parse_route_path(resolved_route_path);
+ const normalized_path = path.endsWith("/") ? path.slice(0, -1) : path;
+ if (!normalized_path) return resolved_route_path;
+ if (
+ normalized_path === file_name
+ || normalized_path.endsWith(`/${file_name}`)
+ || has_file_extension(normalized_path, file_exts)
+ ) return `${normalized_path}${query}${hash}`;
+ return `${normalized_path}/${file_name}${query}${hash}`;
+};
+
+export const resolve_wasm_path = (wasm_path: string | undefined, wasm_file: string, default_wasm_path: string): string =>
+ resolve_route_path(wasm_path, wasm_file, default_wasm_path, [".wasm"]);