@sanvika/geolocation 0.1.0 → 0.2.1
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/package.json +12 -6
- package/src/index.js +12 -0
- package/src/server/index.js +17 -0
- package/src/server/india.js +46 -0
- package/src/server/stateDetection.js +112 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanvika/geolocation",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Sanvika ecosystem geolocation SDK —
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Sanvika ecosystem geolocation SDK — IP lookup, browser geolocation, reverse geocoding (lat/lng → Indian state/city), Indian states constants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"module": "./src/index.js",
|
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
"src",
|
|
15
15
|
"README.md"
|
|
16
16
|
],
|
|
17
|
-
"
|
|
18
|
-
"@sanvika/logger": ">=0.
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@sanvika/logger": ">=0.2"
|
|
19
19
|
},
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"sanvika",
|
|
22
|
+
"geolocation",
|
|
23
|
+
"ip",
|
|
24
|
+
"location",
|
|
25
|
+
"india"
|
|
26
|
+
],
|
|
21
27
|
"author": "Sanvika Production",
|
|
22
28
|
"license": "UNLICENSED",
|
|
23
29
|
"repository": {
|
|
@@ -27,4 +33,4 @@
|
|
|
27
33
|
"publishConfig": {
|
|
28
34
|
"access": "public"
|
|
29
35
|
}
|
|
30
|
-
}
|
|
36
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,3 +4,15 @@
|
|
|
4
4
|
// Client exports: import from "@sanvika/geolocation/client"
|
|
5
5
|
|
|
6
6
|
export { getLocationFromIp, clearLocationCache, isPrivateIp } from "./server/index.js";
|
|
7
|
+
export {
|
|
8
|
+
detectStateFromCoordinates,
|
|
9
|
+
isUserInTargetStates,
|
|
10
|
+
clearStateCache,
|
|
11
|
+
getStateCacheStats,
|
|
12
|
+
INDIAN_STATES,
|
|
13
|
+
INDIAN_UNION_TERRITORIES,
|
|
14
|
+
ALL_INDIAN_REGIONS,
|
|
15
|
+
STATE_NAME_ALIASES,
|
|
16
|
+
normalizeStateName,
|
|
17
|
+
isValidIndianState,
|
|
18
|
+
} from "./server/index.js";
|
package/src/server/index.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// packages/sanvika-geolocation-sdk/src/server/index.js
|
|
2
2
|
// Server-side IP geolocation via ip-api.com (free, no API key, 45 req/min)
|
|
3
|
+
// + Reverse geocoding: lat/lng → Indian state/city via Google Geocoding API
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
detectStateFromCoordinates,
|
|
7
|
+
isUserInTargetStates,
|
|
8
|
+
clearStateCache,
|
|
9
|
+
getStateCacheStats,
|
|
10
|
+
} from "./stateDetection.js";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
INDIAN_STATES,
|
|
14
|
+
INDIAN_UNION_TERRITORIES,
|
|
15
|
+
ALL_INDIAN_REGIONS,
|
|
16
|
+
STATE_NAME_ALIASES,
|
|
17
|
+
normalizeStateName,
|
|
18
|
+
isValidIndianState,
|
|
19
|
+
} from "./india.js";
|
|
3
20
|
|
|
4
21
|
import { createLogger } from "@sanvika/logger";
|
|
5
22
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Official Indian States and Union Territories
|
|
2
|
+
// Source: https://en.wikipedia.org/wiki/States_and_union_territories_of_India
|
|
3
|
+
|
|
4
|
+
export const INDIAN_STATES = [
|
|
5
|
+
"Andhra Pradesh", "Arunachal Pradesh", "Assam", "Bihar", "Chhattisgarh",
|
|
6
|
+
"Goa", "Gujarat", "Haryana", "Himachal Pradesh", "Jharkhand", "Karnataka",
|
|
7
|
+
"Kerala", "Madhya Pradesh", "Maharashtra", "Manipur", "Meghalaya", "Mizoram",
|
|
8
|
+
"Nagaland", "Odisha", "Punjab", "Rajasthan", "Sikkim", "Tamil Nadu",
|
|
9
|
+
"Telangana", "Tripura", "Uttar Pradesh", "Uttarakhand", "West Bengal",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const INDIAN_UNION_TERRITORIES = [
|
|
13
|
+
"Andaman and Nicobar Islands", "Chandigarh",
|
|
14
|
+
"Dadra and Nagar Haveli and Daman and Diu", "Delhi",
|
|
15
|
+
"Jammu and Kashmir", "Ladakh", "Lakshadweep", "Puducherry",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const ALL_INDIAN_REGIONS = [...INDIAN_STATES, ...INDIAN_UNION_TERRITORIES].sort();
|
|
19
|
+
|
|
20
|
+
// Maps Google Geocoding API variations → official name
|
|
21
|
+
export const STATE_NAME_ALIASES = {
|
|
22
|
+
"Orissa": "Odisha",
|
|
23
|
+
"Uttaranchal": "Uttarakhand",
|
|
24
|
+
"NCT of Delhi": "Delhi",
|
|
25
|
+
"National Capital Territory of Delhi": "Delhi",
|
|
26
|
+
"Pondicherry": "Puducherry",
|
|
27
|
+
"Dadra and Nagar Haveli": "Dadra and Nagar Haveli and Daman and Diu",
|
|
28
|
+
"Daman and Diu": "Dadra and Nagar Haveli and Daman and Diu",
|
|
29
|
+
"Jammu & Kashmir": "Jammu and Kashmir",
|
|
30
|
+
"Andaman & Nicobar Islands": "Andaman and Nicobar Islands",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function normalizeStateName(stateName) {
|
|
34
|
+
if (!stateName || typeof stateName !== "string") return null;
|
|
35
|
+
const trimmed = stateName.trim();
|
|
36
|
+
const direct = ALL_INDIAN_REGIONS.find((s) => s.toLowerCase() === trimmed.toLowerCase());
|
|
37
|
+
if (direct) return direct;
|
|
38
|
+
const aliasKey = Object.keys(STATE_NAME_ALIASES).find(
|
|
39
|
+
(k) => k.toLowerCase() === trimmed.toLowerCase()
|
|
40
|
+
);
|
|
41
|
+
return aliasKey ? STATE_NAME_ALIASES[aliasKey] : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isValidIndianState(stateName) {
|
|
45
|
+
return normalizeStateName(stateName) !== null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Reverse geocoding: lat/lng → Indian state + city via Google Geocoding API
|
|
2
|
+
// Supports GOOGLE_MAPS_API_KEY (server-safe) or NEXT_PUBLIC_GOOGLE_MAPS_API_KEY (Next.js)
|
|
3
|
+
|
|
4
|
+
import { createLogger } from "@sanvika/logger";
|
|
5
|
+
import { normalizeStateName } from "./india.js";
|
|
6
|
+
|
|
7
|
+
const logger = createLogger({ namespace: "SanvikaGeolocation.StateDetection" });
|
|
8
|
+
|
|
9
|
+
// In-memory cache — key: "lat_lng" rounded to 2dp (~1.1km accuracy)
|
|
10
|
+
const stateCache = new Map();
|
|
11
|
+
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
12
|
+
|
|
13
|
+
function getCacheKey(lat, lng) {
|
|
14
|
+
return `${Math.round(lat * 100) / 100}_${Math.round(lng * 100) / 100}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getApiKey() {
|
|
18
|
+
return (
|
|
19
|
+
(typeof process !== "undefined" && process.env?.GOOGLE_MAPS_API_KEY) ||
|
|
20
|
+
(typeof process !== "undefined" && process.env?.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY) ||
|
|
21
|
+
null
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function detectStateFromCoordinates(latitude, longitude) {
|
|
26
|
+
const lat = typeof latitude === "string" ? parseFloat(latitude) : latitude;
|
|
27
|
+
const lng = typeof longitude === "string" ? parseFloat(longitude) : longitude;
|
|
28
|
+
|
|
29
|
+
if (!lat || !lng || !Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
30
|
+
logger.warn("Invalid coordinates", { latitude, longitude });
|
|
31
|
+
return { state: null, city: null, fromCache: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const key = getCacheKey(lat, lng);
|
|
35
|
+
const cached = stateCache.get(key);
|
|
36
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
37
|
+
return { state: cached.state, city: cached.city, fromCache: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const apiKey = getApiKey();
|
|
41
|
+
if (!apiKey) {
|
|
42
|
+
logger.error("Google Maps API key not configured (set GOOGLE_MAPS_API_KEY or NEXT_PUBLIC_GOOGLE_MAPS_API_KEY)");
|
|
43
|
+
return { state: null, city: null, fromCache: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
|
48
|
+
url.searchParams.set("latlng", `${lat},${lng}`);
|
|
49
|
+
url.searchParams.set("key", apiKey);
|
|
50
|
+
url.searchParams.set("result_type", "administrative_area_level_1|locality");
|
|
51
|
+
|
|
52
|
+
const res = await fetch(url.toString(), {
|
|
53
|
+
signal: AbortSignal.timeout(5000),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!res.ok) throw new Error(`Geocoding API returned ${res.status}`);
|
|
57
|
+
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
|
|
60
|
+
if (data.status !== "OK") {
|
|
61
|
+
logger.warn("Geocoding API non-OK status", { status: data.status, lat, lng });
|
|
62
|
+
return { state: null, city: null, fromCache: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let rawState = null;
|
|
66
|
+
let city = null;
|
|
67
|
+
|
|
68
|
+
for (const result of (data.results || [])) {
|
|
69
|
+
for (const component of (result.address_components || [])) {
|
|
70
|
+
if (component.types.includes("administrative_area_level_1") && !rawState) {
|
|
71
|
+
rawState = component.long_name;
|
|
72
|
+
}
|
|
73
|
+
if (component.types.includes("locality") && !city) {
|
|
74
|
+
city = component.long_name;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (rawState && city) break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const state = normalizeStateName(rawState);
|
|
81
|
+
|
|
82
|
+
if (state) {
|
|
83
|
+
logger.info("State detected", { state, city, rawState });
|
|
84
|
+
} else if (rawState) {
|
|
85
|
+
logger.warn("State not recognized as Indian state", { rawState });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stateCache.set(key, { state, city, rawState, timestamp: Date.now() });
|
|
89
|
+
return { state, city, fromCache: false };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
logger.error("State detection failed", { error: error.message, lat, lng });
|
|
92
|
+
return { state: null, city: null, fromCache: false };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function isUserInTargetStates(latitude, longitude, targetStates) {
|
|
97
|
+
if (!Array.isArray(targetStates) || targetStates.length === 0) return false;
|
|
98
|
+
const { state } = await detectStateFromCoordinates(latitude, longitude);
|
|
99
|
+
if (!state) return false;
|
|
100
|
+
const normalized = targetStates.map(normalizeStateName).filter(Boolean);
|
|
101
|
+
const match = normalized.includes(state);
|
|
102
|
+
logger.debug("State targeting", { state, targetStates: normalized, match });
|
|
103
|
+
return match;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function clearStateCache() {
|
|
107
|
+
stateCache.clear();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getStateCacheStats() {
|
|
111
|
+
return { keys: stateCache.size };
|
|
112
|
+
}
|