@code-plate/iran-cities 1.0.2 → 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.fa.md CHANGED
@@ -26,8 +26,10 @@ pnpm add @code-plate/iran-cities
26
26
  ## داده‌ها
27
27
 
28
28
  - `۳۱` استان
29
- - `۱۲۵۳` شهر
30
- - هر رکورد شامل `en` (شناسه لاتین kebab-case، مثل `tehran`) و `fa` (نام فارسی، مثل `تهران`) است.
29
+ - `۱۲۵۲` شهر
30
+ - هر رکورد شامل `id` (شناسه‌ی عددی یکتا و پایدار)، `en` (شناسه لاتین kebab-case، مثل `tehran`) و `fa` (نام فارسی، مثل `تهران`) است.
31
+ - هر استان `phoneCode` (کد تلفن بین‌شهری) و `capitalId` (id مرکز استان) دارد؛ هر شهر `isCapital` دارد و مراکز استان‌ها `coordinates: { lat, lng }` هم دارند.
32
+ - `id` شهرها در **کل کشور یکتاست** (شهرهای هم‌نام مثل «مهاباد» در دو استان، دو id متفاوت دارند) و بین نسخه‌ها تغییر نمی‌کند؛ شهر جدید همیشه id بعدی را می‌گیرد.
31
33
 
32
34
  ---
33
35
 
@@ -40,6 +42,9 @@ import {
40
42
  getProvince,
41
43
  getCities,
42
44
  getCity,
45
+ getCityById,
46
+ getProvinceById,
47
+ getCitiesByName,
43
48
  getAllCities,
44
49
  getProvinceOfCity,
45
50
  searchProvinces,
@@ -49,15 +54,16 @@ import {
49
54
 
50
55
  // همه‌ی استان‌ها به همراه شهرهایشان
51
56
  getProvinces();
52
- // [{ en: 'alborz', fa: 'البرز', cities: [{ en: 'karaj', fa: 'کرج' }, ...] }, ...]
57
+ // [{ id: 1, en: 'alborz', fa: 'البرز', cities: [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj' }, ...] }, ...]
53
58
 
54
59
  // فقط لیست استان‌ها (بدون شهر) - برای Select/Dropdown
55
60
  getProvincesList();
56
- // [{ en: 'alborz', fa: 'البرز' }, { en: 'ardabil', fa: 'اردبیل' }, ...]
61
+ // [{ id: 1, en: 'alborz', fa: 'البرز' }, { id: 2, en: 'ardabil', fa: 'اردبیل' }, ...]
57
62
 
58
63
  // گرفتن یک استان با نام انگلیسی یا فارسی
59
64
  getProvince('tehran');
60
65
  getProvince('تهران'); // همین نتیجه را می‌دهد
66
+ getProvince(28); // با id عددی هم کار می‌کند (رشته‌ی عددی '28' هم قبول است)
61
67
 
62
68
  // شهرهای یک استان خاص
63
69
  getCities('fars'); // بر اساس شناسه انگلیسی
@@ -69,18 +75,33 @@ getCity('شیراز', 'fars'); // محدود به استان فارس
69
75
 
70
76
  // لیست مسطح (flat) همه‌ی شهرهای کشور همراه با استان مربوطه
71
77
  getAllCities();
72
- // [{ en: 'karaj', fa: 'کرج', provinceEn: 'alborz', provinceFa: 'البرز' }, ...]
78
+ // [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj', provinceId: 1, provinceEn: 'alborz', provinceFa: 'البرز' }, ...]
79
+
80
+ // گرفتن شهر/استان با id
81
+ getCityById(292); // => شیراز (provinceId: 7)
82
+ getProvinceById(7); // => فارس
83
+
84
+ // نام‌های تکراری: همه‌ی شهرهای با این نام
85
+ getCitiesByName('مهاباد'); // => [اصفهان/مهاباد (id 270), آذربایجان غربی/مهاباد (id 1171)]
73
86
 
74
87
  // پیدا کردن استانِ یک شهر
75
- getProvinceOfCity('اصفهان'); // => { en: 'isfahan', fa: 'اصفهان', cities: [...] }
88
+ getProvinceOfCity('اصفهان'); // => { id: 6, en: 'isfahan', fa: 'اصفهان', cities: [...] }
89
+ getProvinceOfCity(292); // با id شهر هم کار می‌کند
76
90
 
77
91
  // جستجوی زنده (autocomplete) در استان‌ها یا شهرها
78
92
  searchProvinces('خراسان'); // هر سه استان خراسان را برمی‌گرداند
79
93
  searchCities('شهر', { limit: 10 }); // ۱۰ نتیجه‌ی اول شامل «شهر»
80
94
  searchCities('mashhad', { locale: 'en' }); // جستجو فقط در نام انگلیسی
81
95
 
96
+ // مرکز استان، کد تلفن و مختصات
97
+ getCapital('khuzestan'); // => { id: 674, en: 'ahvaz', isCapital: true, coordinates: { lat: 31.31901, lng: 48.6842 }, phoneCode: '061', ... }
98
+ getCapitals(); // ۳۱ مرکز استان
99
+ getProvinceByPhoneCode('021'); // => تهران ('21' و 21 هم قبول است)
100
+ getCity('نجف‌آباد').phoneCode; // => '031' (کد استانِ شهر)
101
+ getCity('اهواز').coordinates; // => { lat, lng } | برای شهرهای غیرمرکز استان: undefined
102
+
82
103
  // آمار کلی
83
- getStats(); // { provinceCount: 31, cityCount: 1253 }
104
+ getStats(); // { provinceCount: 31, cityCount: 1252 }
84
105
  ```
85
106
 
86
107
  ### نکته‌ی مهم درباره‌ی جستجو
@@ -119,21 +140,25 @@ app.get('/api/cities/search', (req, res) => {
119
140
  import { useProvinces, useCities, useProvinceCitySelect, useCitySearch } from '@code-plate/iran-cities/react';
120
141
 
121
142
  function AddressForm() {
122
- const { provinces, cities, province, setProvince, city, setCity } = useProvinceCitySelect();
143
+ const { provinces, cities, province, setProvince, city, setCity, provinceId, cityId, selectedCity } =
144
+ useProvinceCitySelect();
145
+
146
+ // موقع ارسال فرم: { provinceId, cityId } را به بک‌اند بفرستید
147
+ // selectedCity => { id, en, fa, slug, provinceId, provinceEn, provinceFa }
123
148
 
124
149
  return (
125
150
  <>
126
151
  <select value={province} onChange={(e) => setProvince(e.target.value)}>
127
152
  <option value="">انتخاب استان</option>
128
153
  {provinces.map((p) => (
129
- <option key={p.en} value={p.en}>{p.fa}</option>
154
+ <option key={p.id} value={p.en}>{p.fa}</option>
130
155
  ))}
131
156
  </select>
132
157
 
133
158
  <select value={city} onChange={(e) => setCity(e.target.value)} disabled={!province}>
134
159
  <option value="">انتخاب شهر</option>
135
160
  {cities.map((c) => (
136
- <option key={c.en} value={c.en}>{c.fa}</option>
161
+ <option key={c.id} value={c.en}>{c.fa}</option>
137
162
  ))}
138
163
  </select>
139
164
  </>
@@ -154,7 +179,7 @@ function CityAutocomplete() {
154
179
  <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="نام شهر..." />
155
180
  <ul>
156
181
  {results.map((c) => (
157
- <li key={`${c.provinceEn}-${c.en}`}>{c.fa} ({c.provinceFa})</li>
182
+ <li key={c.id}>{c.fa} ({c.provinceFa})</li>
158
183
  ))}
159
184
  </ul>
160
185
  </div>
@@ -168,7 +193,7 @@ function CityAutocomplete() {
168
193
  |---|---|
169
194
  | `useProvinces()` | لیست استان‌ها |
170
195
  | `useCities(provinceQuery)` | شهرهای یک استان، وابسته به تغییر پارامتر |
171
- | `useProvinceCitySelect()` | state کامل یک فرم استان→شهر (با ریست خودکار شهر هنگام تغییر استان) |
196
+ | `useProvinceCitySelect()` | state کامل یک فرم استان→شهر (با ریست خودکار شهر هنگام تغییر استان)؛ علاوه بر `province`/`city` مقدارهای `provinceId`, `cityId`, `selectedProvince`, `selectedCity` و `reset()` را هم برمی‌گرداند |
172
197
  | `useCitySearch(initialQuery?, options?)` | state جستجوی زنده‌ی شهر |
173
198
  | `useProvinceSearch(initialQuery?, options?)` | state جستجوی زنده‌ی استان |
174
199
 
@@ -224,12 +249,18 @@ export class IranLocationsService {
224
249
  | تابع | ورودی | خروجی | توضیح |
225
250
  |---|---|---|---|
226
251
  | `getProvinces()` | - | `Province[]` | همه‌ی استان‌ها به همراه شهرها |
227
- | `getProvincesList()` | - | `{en, fa}[]` | فقط لیست استان‌ها |
228
- | `getProvince(query)` | `string` | `Province \| undefined` | یک استان با en یا fa |
229
- | `getCities(provinceQuery)` | `string` | `City[]` | شهرهای یک استان |
230
- | `getCity(cityQuery, provinceQuery?)` | `string, string?` | `FlatCity \| undefined` | یک شهر مشخص |
252
+ | `getProvincesList()` | - | `{id, en, fa}[]` | فقط لیست استان‌ها |
253
+ | `getCapital(provinceQuery)` | `string \| number` | `FlatCity \| undefined` | مرکز استان |
254
+ | `getCapitals()` | - | `FlatCity[]` | مراکز ۳۱ استان |
255
+ | `getProvinceByPhoneCode(code)` | `string \| number` | `Province \| undefined` | استان با کد تلفن بین‌شهری |
256
+ | `getProvinceById(id)` | `number` | `Province \| undefined` | استان با id |
257
+ | `getCityById(id)` | `number` | `FlatCity \| undefined` | شهر با id سراسری |
258
+ | `getCitiesByName(name)` | `string` | `FlatCity[]` | همه‌ی شهرهای با این نام (برای نام‌های تکراری) |
259
+ | `getProvince(query)` | `string \| number` | `Province \| undefined` | یک استان با en یا fa |
260
+ | `getCities(provinceQuery)` | `string \| number` | `City[]` | شهرهای یک استان |
261
+ | `getCity(cityQuery, provinceQuery?)` | `string \| number, (string \| number)?` | `FlatCity \| undefined` | یک شهر مشخص |
231
262
  | `getAllCities()` | - | `FlatCity[]` | همه‌ی شهرها به‌صورت مسطح |
232
- | `getProvinceOfCity(cityQuery)` | `string` | `Province \| undefined` | استانِ یک شهر |
263
+ | `getProvinceOfCity(cityQuery)` | `string \| number` | `Province \| undefined` | استانِ یک شهر |
233
264
  | `searchProvinces(query, options?)` | `string, SearchOptions?` | `Province[]` | جستجوی fuzzy استان |
234
265
  | `searchCities(query, options?)` | `string, SearchOptions?` | `FlatCity[]` | جستجوی fuzzy شهر |
235
266
  | `getStats()` | - | `{provinceCount, cityCount}` | آمار کلی |
@@ -239,9 +270,10 @@ export class IranLocationsService {
239
270
  ### تایپ‌ها
240
271
 
241
272
  ```ts
242
- interface City { en: string; fa: string; }
243
- interface Province { en: string; fa: string; cities: City[]; }
244
- interface FlatCity extends City { provinceEn: string; provinceFa: string; }
273
+ interface Coordinates { lat: number; lng: number; }
274
+ interface City { id: number; isCapital: boolean; coordinates?: Coordinates; slug: string; en: string; fa: string; }
275
+ interface Province { id: number; en: string; fa: string; phoneCode: string; capitalId: number; cities: City[]; }
276
+ interface FlatCity extends City { provinceId: number; provinceEn: string; provinceFa: string; phoneCode: string; }
245
277
  ```
246
278
 
247
279
  ---
@@ -252,8 +284,12 @@ interface FlatCity extends City { provinceEn: string; provinceFa: string; }
252
284
  npm install
253
285
  npm run build # خروجی dist/ (cjs + esm + d.ts)
254
286
  npm run typecheck
287
+ npm run validate # اعتبارسنجی یکپارچگی data.json (id، slug، مرکز استان، کد تلفن، مختصات)
288
+ npm test # validate + typecheck
255
289
  ```
256
290
 
291
+ > **مختصات:** برای ۳۷۰ شهر (۳۱ مرکز استان + عمدتاً مراکز شهرستان) موجود است و فقط وقتی ثبت شده که حداقل دو منبع مستقل روی آن توافق داشته‌اند (جزئیات در `reports/`). برای بقیه `coordinates` برابر `undefined` است و هیچ مقدار تقریبی جایگزین نمی‌شود. منبع داده: GeoNames (CC BY 4.0)، فایل `NOTICE.md`.
292
+
257
293
  ## لایسنس
258
294
 
259
295
  MIT
package/README.md CHANGED
@@ -26,8 +26,10 @@ pnpm add @code-plate/iran-cities
26
26
  ## Data
27
27
 
28
28
  - `31` provinces
29
- - `1253` cities
30
- - Every record has an `en` field (latin, kebab-case identifier, e.g. `tehran`) and an `fa` field (Persian name, e.g. `تهران`).
29
+ - `1252` cities
30
+ - Every record has an `id` (stable numeric identifier), an `en` field (latin, kebab-case identifier, e.g. `tehran`) and an `fa` field (Persian name, e.g. `تهران`).
31
+ - Every province has a `phoneCode` (area code) and a `capitalId`; every city has `isCapital`, and province capitals also carry `coordinates: { lat, lng }`.
32
+ - City `id`s are **globally unique** (same-named cities such as «Mahabad» in two provinces get different ids) and never change between versions; new cities always get the next id.
31
33
 
32
34
  ---
33
35
 
@@ -40,6 +42,9 @@ import {
40
42
  getProvince,
41
43
  getCities,
42
44
  getCity,
45
+ getCityById,
46
+ getProvinceById,
47
+ getCitiesByName,
43
48
  getAllCities,
44
49
  getProvinceOfCity,
45
50
  searchProvinces,
@@ -49,15 +54,16 @@ import {
49
54
 
50
55
  // All provinces along with their cities
51
56
  getProvinces();
52
- // [{ en: 'alborz', fa: 'البرز', cities: [{ en: 'karaj', fa: 'کرج' }, ...] }, ...]
57
+ // [{ id: 1, en: 'alborz', fa: 'البرز', cities: [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj' }, ...] }, ...]
53
58
 
54
59
  // Just the list of provinces (no cities) - for a Select/Dropdown
55
60
  getProvincesList();
56
- // [{ en: 'alborz', fa: 'البرز' }, { en: 'ardabil', fa: 'اردبیل' }, ...]
61
+ // [{ id: 1, en: 'alborz', fa: 'البرز' }, { id: 2, en: 'ardabil', fa: 'اردبیل' }, ...]
57
62
 
58
63
  // Get a single province by English id or Persian name
59
64
  getProvince('tehran');
60
65
  getProvince('تهران'); // same result
66
+ getProvince(28); // numeric id works too (so does the string '28')
61
67
 
62
68
  // Cities of a specific province
63
69
  getCities('fars'); // by English id
@@ -69,18 +75,33 @@ getCity('شیراز', 'fars'); // scoped to Fars province
69
75
 
70
76
  // Flat list of every city in the country, along with its province
71
77
  getAllCities();
72
- // [{ en: 'karaj', fa: 'کرج', provinceEn: 'alborz', provinceFa: 'البرز' }, ...]
78
+ // [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj', provinceId: 1, provinceEn: 'alborz', provinceFa: 'البرز' }, ...]
79
+
80
+ // Look up by id
81
+ getCityById(292); // => Shiraz (provinceId: 7)
82
+ getProvinceById(7); // => Fars
83
+
84
+ // Duplicate names: every city with that name
85
+ getCitiesByName('مهاباد'); // => [Isfahan/Mahabad (id 270), West Azerbaijan/Mahabad (id 1171)]
73
86
 
74
87
  // Find which province a city belongs to
75
- getProvinceOfCity('اصفهان'); // => { en: 'isfahan', fa: 'اصفهان', cities: [...] }
88
+ getProvinceOfCity('اصفهان'); // => { id: 6, en: 'isfahan', fa: 'اصفهان', cities: [...] }
89
+ getProvinceOfCity(292); // city id works too
76
90
 
77
91
  // Live search (autocomplete) over provinces or cities
78
92
  searchProvinces('خراسان'); // returns all three "Khorasan" provinces
79
93
  searchCities('شهر', { limit: 10 }); // first 10 results containing "شهر"
80
94
  searchCities('mashhad', { locale: 'en' }); // search English names only
81
95
 
96
+ // Capital, area code and coordinates
97
+ getCapital('khuzestan'); // => { id: 674, en: 'ahvaz', isCapital: true, coordinates: { lat: 31.31901, lng: 48.6842 }, phoneCode: '061', ... }
98
+ getCapitals(); // the 31 provincial capitals
99
+ getProvinceByPhoneCode('021'); // => Tehran ('21' and 21 work too)
100
+ getCity('نجف‌آباد').phoneCode; // => '031' (area code of the city's province)
101
+ getCity('اهواز').coordinates; // => { lat, lng } | undefined for non-capital cities
102
+
82
103
  // Overall stats
83
- getStats(); // { provinceCount: 31, cityCount: 1253 }
104
+ getStats(); // { provinceCount: 31, cityCount: 1252 }
84
105
  ```
85
106
 
86
107
  ### A note on search
@@ -119,21 +140,25 @@ An optional subpath is available for React with a few ready-made hooks:
119
140
  import { useProvinces, useCities, useProvinceCitySelect, useCitySearch } from '@code-plate/iran-cities/react';
120
141
 
121
142
  function AddressForm() {
122
- const { provinces, cities, province, setProvince, city, setCity } = useProvinceCitySelect();
143
+ const { provinces, cities, province, setProvince, city, setCity, provinceId, cityId, selectedCity } =
144
+ useProvinceCitySelect();
145
+
146
+ // On submit, send { provinceId, cityId } to your backend.
147
+ // selectedCity => { id, en, fa, slug, provinceId, provinceEn, provinceFa }
123
148
 
124
149
  return (
125
150
  <>
126
151
  <select value={province} onChange={(e) => setProvince(e.target.value)}>
127
152
  <option value="">Select a province</option>
128
153
  {provinces.map((p) => (
129
- <option key={p.en} value={p.en}>{p.fa}</option>
154
+ <option key={p.id} value={p.en}>{p.fa}</option>
130
155
  ))}
131
156
  </select>
132
157
 
133
158
  <select value={city} onChange={(e) => setCity(e.target.value)} disabled={!province}>
134
159
  <option value="">Select a city</option>
135
160
  {cities.map((c) => (
136
- <option key={c.en} value={c.en}>{c.fa}</option>
161
+ <option key={c.id} value={c.en}>{c.fa}</option>
137
162
  ))}
138
163
  </select>
139
164
  </>
@@ -154,7 +179,7 @@ function CityAutocomplete() {
154
179
  <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="City name..." />
155
180
  <ul>
156
181
  {results.map((c) => (
157
- <li key={`${c.provinceEn}-${c.en}`}>{c.fa} ({c.provinceFa})</li>
182
+ <li key={c.id}>{c.fa} ({c.provinceFa})</li>
158
183
  ))}
159
184
  </ul>
160
185
  </div>
@@ -168,7 +193,7 @@ Hooks available in the `react` subpath:
168
193
  |---|---|
169
194
  | `useProvinces()` | List of provinces |
170
195
  | `useCities(provinceQuery)` | Cities of a province, reacts to changes in the parameter |
171
- | `useProvinceCitySelect()` | Full state + logic for a province→city form field (auto-resets the city when the province changes) |
196
+ | `useProvinceCitySelect()` | Full state + logic for a province→city form field (auto-resets the city when the province changes); also returns `provinceId`, `cityId`, `selectedProvince`, `selectedCity` and `reset()` |
172
197
  | `useCitySearch(initialQuery?, options?)` | Live city search state |
173
198
  | `useProvinceSearch(initialQuery?, options?)` | Live province search state |
174
199
 
@@ -224,12 +249,18 @@ export class IranLocationsService {
224
249
  | Function | Input | Output | Description |
225
250
  |---|---|---|---|
226
251
  | `getProvinces()` | - | `Province[]` | All provinces with their cities |
227
- | `getProvincesList()` | - | `{en, fa}[]` | Provinces only |
228
- | `getProvince(query)` | `string` | `Province \| undefined` | A province by en or fa |
229
- | `getCities(provinceQuery)` | `string` | `City[]` | Cities of a province |
230
- | `getCity(cityQuery, provinceQuery?)` | `string, string?` | `FlatCity \| undefined` | A specific city |
252
+ | `getProvincesList()` | - | `{id, en, fa}[]` | Provinces only |
253
+ | `getCapital(provinceQuery)` | `string \| number` | `FlatCity \| undefined` | Capital of a province |
254
+ | `getCapitals()` | - | `FlatCity[]` | The 31 provincial capitals |
255
+ | `getProvinceByPhoneCode(code)` | `string \| number` | `Province \| undefined` | Province by area code |
256
+ | `getProvinceById(id)` | `number` | `Province \| undefined` | Province by id |
257
+ | `getCityById(id)` | `number` | `FlatCity \| undefined` | City by global id |
258
+ | `getCitiesByName(name)` | `string` | `FlatCity[]` | Every city with that name (for duplicate names) |
259
+ | `getProvince(query)` | `string \| number` | `Province \| undefined` | A province by en or fa |
260
+ | `getCities(provinceQuery)` | `string \| number` | `City[]` | Cities of a province |
261
+ | `getCity(cityQuery, provinceQuery?)` | `string \| number, (string \| number)?` | `FlatCity \| undefined` | A specific city |
231
262
  | `getAllCities()` | - | `FlatCity[]` | All cities, flattened |
232
- | `getProvinceOfCity(cityQuery)` | `string` | `Province \| undefined` | The province a city belongs to |
263
+ | `getProvinceOfCity(cityQuery)` | `string \| number` | `Province \| undefined` | The province a city belongs to |
233
264
  | `searchProvinces(query, options?)` | `string, SearchOptions?` | `Province[]` | Fuzzy province search |
234
265
  | `searchCities(query, options?)` | `string, SearchOptions?` | `FlatCity[]` | Fuzzy city search |
235
266
  | `getStats()` | - | `{provinceCount, cityCount}` | Overall stats |
@@ -239,9 +270,10 @@ export class IranLocationsService {
239
270
  ### Types
240
271
 
241
272
  ```ts
242
- interface City { en: string; fa: string; }
243
- interface Province { en: string; fa: string; cities: City[]; }
244
- interface FlatCity extends City { provinceEn: string; provinceFa: string; }
273
+ interface Coordinates { lat: number; lng: number; }
274
+ interface City { id: number; isCapital: boolean; coordinates?: Coordinates; slug: string; en: string; fa: string; }
275
+ interface Province { id: number; en: string; fa: string; phoneCode: string; capitalId: number; cities: City[]; }
276
+ interface FlatCity extends City { provinceId: number; provinceEn: string; provinceFa: string; phoneCode: string; }
245
277
  ```
246
278
 
247
279
  ---
@@ -252,8 +284,12 @@ interface FlatCity extends City { provinceEn: string; provinceFa: string; }
252
284
  npm install
253
285
  npm run build # outputs dist/ (cjs + esm + d.ts)
254
286
  npm run typecheck
287
+ npm run validate # data integrity checks for data.json (ids, slugs, capitals, area codes, coordinates)
288
+ npm test # validate + typecheck
255
289
  ```
256
290
 
291
+ > **Coordinates:** available for 370 cities (31 provincial capitals + mostly county seats), recorded only when at least two independent sources agree (details in `reports/`). For every other city `coordinates` is `undefined`; no approximate values are filled in. Data source: GeoNames (CC BY 4.0), see `NOTICE.md`.
292
+
257
293
  ## License
258
294
 
259
295
  MIT