geo-sl 1.0.1 → 1.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/README.md CHANGED
@@ -12,11 +12,11 @@
12
12
 
13
13
  ## 🗺️ 100% Comprehensive Island-wide Coverage
14
14
 
15
- `geo-sl` is not limited to major metropolitan areas. It provides complete, authoritative, and verified geographic coverage across the entire territory of Sri Lanka—from provincial capitals down to individual rural villages:
15
+ `geo-sl` covers the entire territory of Sri Lanka — not just major cities and urban centers. It provides complete, authoritative, and verified geographic data from provincial capitals all the way down to individual rural villages and remote Grama Niladhari divisions:
16
16
 
17
17
  | Administrative / Geographic Level | Total Count | Scope & Details | Supported Languages |
18
18
  |---|---|---|---|
19
- | **Provinces** | **9** | All 9 provinces (WP, CP, SP, NP, EP, NW, NC, UV, SG) | English, සිංහල, தமிழ் |
19
+ | **Provinces** | **9** | All 9 provinces (WP, CP, SP, NP, EP, NWP, NCP, UP, SGP) | English, සිංහල, தமிழ் |
20
20
  | **Districts** | **25** | All 25 administrative districts across the island | English, සිංහල, தமிழ் |
21
21
  | **Divisional Secretariats (DSD)** | **340** | 100% of Divisional Secretariat Divisions (MOHA) | English, සිංහල, தமிழ் |
22
22
  | **Grama Niladhari (GN) Divisions** | **14,020** | Every single village, ward, and local community | English, සිංහල, தமிழ் |
@@ -92,7 +92,7 @@ getPostalCode('මහනුවර'); // => "20000"
92
92
  // 5. Search with Relevance Ranking
93
93
  search('colombo'); // => [ { name_en: 'Colombo 1', ... }, { name_en: 'Colombo 2', ... } ]
94
94
  search('nawala'); // => [ { name_en: 'Nawala', ... }, ... ]
95
- search('කොළඹ'); // Sinhala search
95
+ search('කොළඹ'); // => [ { name_en: 'Colombo 1', ... }, ... ] (Sinhala search)
96
96
  ```
97
97
 
98
98
  ---
@@ -121,18 +121,22 @@ import { BANKS, getBanks, getBankByCode, getBranches } from 'geo-sl/banks';
121
121
  import { GN_DIVISIONS, getGNDivisions, searchGN } from 'geo-sl/gn';
122
122
 
123
123
  // Validators & Parsers (~2.3 KB)
124
- import { validateNIC, parseNIC, validatePhone, parsePhone, validatePostalCode } from 'geo-sl/validators';
124
+ import { validateNIC, parseNIC, convertOldNICToNew, validatePhone, parsePhone, formatPhone, validatePostalCode } from 'geo-sl/validators';
125
125
  ```
126
126
 
127
127
  ---
128
128
 
129
- ## 🎨 Interactive React / Next.js Cascading Form Example
129
+ ## 🎨 Interactive React / Next.js Cascading Form Examples
130
130
 
131
- The easiest way to build a Sri Lankan checkout address form:
131
+ `geo-sl` makes building multi-level dependent dropdowns straightforward for both e-commerce checkouts and official KYC/government forms.
132
+
133
+ ### 1. Delivery & Checkout Address Form (`Province ➔ District ➔ City / Postal Code`)
134
+
135
+ For shipping and delivery addresses, use `getCascadingData()` or `toSelectOptions()`:
132
136
 
133
137
  ```tsx
134
138
  import React, { useState } from 'react';
135
- import { getCascadingData } from 'geo-sl';
139
+ import { getCascadingData, toSelectOptions } from 'geo-sl';
136
140
 
137
141
  const addressData = getCascadingData({ lang: 'en' });
138
142
 
@@ -144,22 +148,32 @@ export function SriLankaAddressForm() {
144
148
  const currentProvince = addressData.find((p) => p.code === selectedProvince);
145
149
  const currentDistrict = currentProvince?.districts.find((d) => d.code === selectedDistrict);
146
150
 
151
+ // Convert to standard { label, value } options for React-Select / Shadcn / HTML select
152
+ const provinceOptions = toSelectOptions(addressData, 'name', 'code');
153
+ const districtOptions = toSelectOptions(currentProvince?.districts || [], 'name', 'code');
154
+ const cityOptions = toSelectOptions(
155
+ currentDistrict?.cities || [],
156
+ (c) => `${c.name} (${c.postal_code})`,
157
+ 'name'
158
+ );
159
+
147
160
  return (
148
161
  <div className="space-y-4">
149
162
  {/* Province */}
150
163
  <select
151
164
  value={selectedProvince}
152
165
  onChange={(e) => {
153
- setSelectedProvince(e.target.value as any);
154
- const prov = addressData.find((p) => p.code === e.target.value);
166
+ const code = e.target.value;
167
+ setSelectedProvince(code as any);
168
+ const prov = addressData.find((p) => p.code === code);
155
169
  if (prov && prov.districts[0]) {
156
170
  setSelectedDistrict(prov.districts[0].code);
157
171
  setSelectedCity(prov.districts[0].cities[0]?.name || '');
158
172
  }
159
173
  }}
160
174
  >
161
- {addressData.map((p) => (
162
- <option key={p.code} value={p.code}>{p.name}</option>
175
+ {provinceOptions.map((opt) => (
176
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
163
177
  ))}
164
178
  </select>
165
179
 
@@ -167,15 +181,16 @@ export function SriLankaAddressForm() {
167
181
  <select
168
182
  value={selectedDistrict}
169
183
  onChange={(e) => {
170
- setSelectedDistrict(e.target.value as any);
171
- const dist = currentProvince?.districts.find((d) => d.code === e.target.value);
184
+ const code = e.target.value;
185
+ setSelectedDistrict(code as any);
186
+ const dist = currentProvince?.districts.find((d) => d.code === code);
172
187
  if (dist && dist.cities[0]) {
173
188
  setSelectedCity(dist.cities[0].name);
174
189
  }
175
190
  }}
176
191
  >
177
- {currentProvince?.districts.map((d) => (
178
- <option key={d.code} value={d.code}>{d.name}</option>
192
+ {districtOptions.map((opt) => (
193
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
179
194
  ))}
180
195
  </select>
181
196
 
@@ -184,10 +199,8 @@ export function SriLankaAddressForm() {
184
199
  value={selectedCity}
185
200
  onChange={(e) => setSelectedCity(e.target.value)}
186
201
  >
187
- {currentDistrict?.cities.map((c) => (
188
- <option key={c.name} value={c.name}>
189
- {c.name} ({c.postal_code})
190
- </option>
202
+ {cityOptions.map((opt) => (
203
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
191
204
  ))}
192
205
  </select>
193
206
  </div>
@@ -195,6 +208,94 @@ export function SriLankaAddressForm() {
195
208
  }
196
209
  ```
197
210
 
211
+ ---
212
+
213
+ ### 2. Administrative & Village Form (`Province ➔ District ➔ DS Division ➔ Village / GN`)
214
+
215
+ For banking KYC, voter registration, legal documentation, or government administrative forms, query on-demand down to the **14,020 official villages (Grama Niladhari divisions)**:
216
+
217
+ ```tsx
218
+ import React, { useState, useEffect } from 'react';
219
+ import { getProvinces, getDistrictsByProvince, getDivisionsByDistrict, toSelectOptions } from 'geo-sl';
220
+ import { getVillagesByDivision, searchVillages, type Village } from 'geo-sl/gn';
221
+
222
+ export function SriLankaAdministrativeForm() {
223
+ const [provinceCode, setProvinceCode] = useState('WP');
224
+ const [district, setDistrict] = useState('Colombo');
225
+ const [division, setDivision] = useState('Colombo');
226
+ const [villages, setVillages] = useState<readonly Village[]>([]);
227
+ const [selectedVillage, setSelectedVillage] = useState('');
228
+
229
+ // Load villages dynamically whenever the DS division changes
230
+ useEffect(() => {
231
+ const list = getVillagesByDivision(division);
232
+ setVillages(list);
233
+ if (list.length > 0) setSelectedVillage(list[0].code);
234
+ }, [division]);
235
+
236
+ const provinces = toSelectOptions(getProvinces(), 'name', 'code');
237
+ const districts = toSelectOptions(getDistrictsByProvince(provinceCode), 'name', 'name_en');
238
+ const divisions = toSelectOptions(getDivisionsByDistrict(district), 'name', 'name_en');
239
+ const villageOptions = toSelectOptions(
240
+ villages,
241
+ (v) => `${v.name} (${v.code})`,
242
+ 'code'
243
+ );
244
+
245
+ return (
246
+ <form className="space-y-4">
247
+ {/* 1. Province */}
248
+ <select
249
+ value={provinceCode}
250
+ onChange={(e) => {
251
+ setProvinceCode(e.target.value);
252
+ const firstDist = getDistrictsByProvince(e.target.value)[0];
253
+ if (firstDist) {
254
+ setDistrict(firstDist.name_en);
255
+ const firstDiv = getDivisionsByDistrict(firstDist.name_en)[0];
256
+ if (firstDiv) setDivision(firstDiv.name_en);
257
+ }
258
+ }}
259
+ >
260
+ {provinces.map((p) => <option key={p.value} value={p.value}>{p.label}</option>)}
261
+ </select>
262
+
263
+ {/* 2. District */}
264
+ <select
265
+ value={district}
266
+ onChange={(e) => {
267
+ setDistrict(e.target.value);
268
+ const firstDiv = getDivisionsByDistrict(e.target.value)[0];
269
+ if (firstDiv) setDivision(firstDiv.name_en);
270
+ }}
271
+ >
272
+ {districts.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}
273
+ </select>
274
+
275
+ {/* 3. DS Division */}
276
+ <select
277
+ value={division}
278
+ onChange={(e) => setDivision(e.target.value)}
279
+ >
280
+ {divisions.map((div) => <option key={div.value} value={div.value}>{div.label}</option>)}
281
+ </select>
282
+
283
+ {/* 4. Village (Grama Niladhari Division) */}
284
+ <select
285
+ value={selectedVillage}
286
+ onChange={(e) => setSelectedVillage(e.target.value)}
287
+ >
288
+ {villageOptions.map((v) => <option key={v.value} value={v.value}>{v.label}</option>)}
289
+ </select>
290
+ </form>
291
+ );
292
+ }
293
+ ```
294
+
295
+ > [!TIP]
296
+ > **Autocomplete for Villages:** With 14,000+ villages, you can also use `searchVillages('Mattakkuliya', { district: 'Colombo', limit: 10 })` to power modern searchable comboboxes (Shadcn, Headless UI, Radix) across English, Sinhala (`මට්ටක්කුලිය`), and Tamil (`மட்டக்குளி`).
297
+
298
+
198
299
  ---
199
300
 
200
301
  ## 🏦 CBSL Bank & Branch Codes (`geo-sl/banks`)
@@ -209,7 +310,7 @@ const allBanks = getBanks();
209
310
 
210
311
  // Find Bank of Ceylon
211
312
  const boc = getBankByCode('7010');
212
- console.log(boc.name); // "Bank of Ceylon"
313
+ console.log(boc?.name); // "Bank of Ceylon"
213
314
 
214
315
  // Get all branches for a bank
215
316
  const branches = getBranches('7010');
@@ -222,12 +323,16 @@ const branches = getBranches('7010');
222
323
 
223
324
  ### Provinces
224
325
  * `getProvinces(options?: { lang?: 'en' | 'si' | 'ta' }): Province[]`
225
- * `getProvinceByCode(code: string, options?: { lang?: 'en' | 'si' | 'ta' }): Province | undefined`
326
+ * `getProvince(codeOrId: string): Province | undefined` – primary lookup by code (e.g. `'WP'`), ID, or English name.
327
+ * `getProvinceName(codeOrId: string, lang?: Language): string | undefined`
328
+ * `getProvinceByCode(code: string, options?: { lang?: 'en' | 'si' | 'ta' }): Province | undefined` – alias for `getProvince`
226
329
 
227
330
  ### Districts
228
331
  * `getDistricts(province?: string, options?: { lang?: 'en' | 'si' | 'ta' }): District[]`
229
- * `getDistrictByCode(code: string, options?: { lang?: 'en' | 'si' | 'ta' }): District | undefined`
332
+ * `getDistrict(codeOrId: string): District | undefined` – primary lookup by abbreviation (e.g. `'CO'`), ID, or English name.
333
+ * `getDistrictName(codeOrId: string, lang?: Language): string | undefined`
230
334
  * `getDistrictsByProvince(province: string, options?: { lang?: 'en' | 'si' | 'ta' }): District[]`
335
+ * `getDistrictByCode(code: string, options?: { lang?: 'en' | 'si' | 'ta' }): District | undefined` – alias for `getDistrict`
231
336
 
232
337
  ### Cities & Postal Codes
233
338
  * `getCities(district?: string, options?: { lang?: 'en' | 'si' | 'ta' }): City[]`
@@ -235,36 +340,47 @@ const branches = getBranches('7010');
235
340
  * `getCitiesByProvince(province: string, options?: { lang?: 'en' | 'si' | 'ta' }): City[]`
236
341
  * `getPostalCode(cityName: string): string | undefined`
237
342
  * `getCityByPostalCode(postalCode: string | number, lang?: 'en' | 'si' | 'ta'): City | undefined`
238
- * `isValidPostalCode(postalCode: string | number): boolean`
239
- * `lookupPostalCode(code: string | number, options?: { lang?: 'en' | 'si' | 'ta' }): City | undefined`
240
- * `lookupAllByPostalCode(code: string | number, options?: { lang?: 'en' | 'si' | 'ta' }): City[]`
343
+ * `isValidPostalCode(postalCode: string | number): boolean` – checks existence against the Sri Lanka Post database.
344
+ * `lookupPostalCode(code: string | number, options?: { lang?: 'en' | 'si' | 'ta' }): City | undefined` – alias for `getCityByPostalCode`
345
+ * `lookupAllByPostalCode(code: string | number, options?: { lang?: 'en' | 'si' | 'ta' }): City[]` – returns all offices sharing a postal code.
241
346
  * `search(query: string, options?: { limit?: number; lang?: 'en' | 'si' | 'ta'; district?: string; province?: string }): City[]`
242
347
 
243
- ### Cascading Hierarchy
244
- * `getCascadingData(options?: { lang?: 'en' | 'si' | 'ta' }): CascadingProvince[]`
348
+ ### Cascading Hierarchy & Form Helpers
349
+ * `getCascadingData(options?: { lang?: 'en' | 'si' | 'ta' }): CascadingProvince[]` – pre-nested tree (Province ➔ District ➔ Cities).
350
+ * `getAdministrativeCascadingData(options?: { lang?: 'en' | 'si' | 'ta' }): CascadingAdministrativeProvince[]` – pre-nested tree (Province ➔ District ➔ DS Divisions).
351
+ * `toSelectOptions<T>(items, labelKey, valueKey): SelectOption[]` – universal formatter converting data objects into `{ label, value }` pairs for UI dropdowns.
245
352
 
246
353
  ### Administrative Divisions (MOHA)
247
354
  * `getDivisions(district?: string, options?: { lang?: 'en' | 'si' | 'ta' }): Division[]`
248
355
  * `getDivisionsByDistrict(district: string, options?: { lang?: 'en' | 'si' | 'ta' }): Division[]`
356
+ * `getDivisionsByProvince(province: string, options?: { lang?: 'en' | 'si' | 'ta' }): Division[]`
249
357
 
250
358
  ### Financial Institutions (CBSL / LankaPay)
251
- * `getBanks(): Bank[]`
252
- * `getBankByCode(code: string | number): Bank | undefined`
253
- * `getBranches(bankCode: string | number): Branch[]`
359
+ * `getBanks(): readonly Bank[]`
360
+ * `getBank(codeOrId: string | number): Bank | undefined` – primary lookup by 4-digit CBSL code, ID, or bank name.
361
+ * `getBankByCode(codeOrId: string | number): Bank | undefined` – alias for `getBank`
362
+ * `getBranches(bankCode: string | number): readonly Branch[]`
254
363
  * `getBranchByCode(bankCode: string | number, branchCode: string | number): Branch | undefined`
255
364
  * `searchBranches(bankCode: string | number, query: string): Branch[]`
256
365
 
257
- ### Grama Niladhari (GN) Divisions (`geo-sl/gn`)
366
+ ### Grama Niladhari (GN) & Village Divisions (`geo-sl/gn`)
258
367
  * `getGNDivisions(options?: { lang?: 'en' | 'si' | 'ta' }): GNDivision[]`
259
368
  * `getGNDivisionsByDSD(divisionName: string, options?: { lang?: 'en' | 'si' | 'ta' }): GNDivision[]`
260
369
  * `getGNDivisionsByDistrict(districtName: string, options?: { lang?: 'en' | 'si' | 'ta' }): GNDivision[]`
261
370
  * `findGNByCode(code: string, options?: { lang?: 'en' | 'si' | 'ta' }): GNDivision | undefined`
262
371
  * `searchGN(query: string, options?: { limit?: number; lang?: 'en' | 'si' | 'ta'; district?: string; division?: string }): GNDivision[]`
372
+ * **Village Aliases:**
373
+ * `getVillages(options?)` – alias for `getGNDivisions`
374
+ * `getVillagesByDivision(divisionName, options?)` – alias for `getGNDivisionsByDSD`
375
+ * `getVillagesByDistrict(districtName, options?)` – alias for `getGNDivisionsByDistrict`
376
+ * `findVillageByCode(code, options?)` – alias for `findGNByCode`
377
+ * `searchVillages(query, options?)` – alias for `searchGN`
378
+ * `VILLAGES` – alias for `GN_DIVISIONS`
263
379
 
264
380
  ### Sri Lanka Validators & Parsers (`geo-sl/validators`)
265
381
  * `validateNIC(nic: string): boolean`
266
382
  * `parseNIC(nic: string): ParsedNIC | null` – parses birthdate, gender, age, voter eligibility from Old (9+V/X) and New (12 digits) NICs.
267
- * `convertOldNICToNew(oldNic: string): string | null` – converts 9-digit old NIC to 12-digit format.
383
+ * `convertOldNICToNew(oldNic: string): string | null` – converts 10-character old NIC (9 digits + V/X) to 12-digit new format.
268
384
  * `validatePhone(phone: string): boolean`
269
385
  * `parsePhone(phone: string): ParsedPhone | null` – parses operator (Dialog, Mobitel, Hutch, Airtel), type (mobile/fixed), and formats.
270
386
  * `formatPhone(phone: string, style?: 'international' | 'local' | 'e164'): string | null`
@@ -281,15 +397,21 @@ import type {
281
397
  Bank,
282
398
  Branch,
283
399
  GNDivision,
400
+ Village,
401
+ SelectOption,
284
402
  CascadingProvince,
285
403
  CascadingDistrict,
404
+ CascadingAdministrativeProvince,
405
+ CascadingAdministrativeDistrict,
406
+ CascadingAdministrativeDivision,
286
407
  ParsedNIC,
287
408
  ParsedPhone,
288
409
  Language,
289
410
  ProvinceCode,
290
411
  DistrictCode,
291
412
  QueryOptions,
292
- SearchOptions
413
+ SearchOptions,
414
+ VillageSearchOptions
293
415
  } from 'geo-sl';
294
416
  ```
295
417