@code-plate/iran-cities 1.1.0 → 2.0.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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/MIGRATION.fa.md +277 -0
- package/MIGRATION.md +272 -0
- package/NOTICE.md +38 -0
- package/README.fa.md +528 -201
- package/README.md +524 -201
- package/dist/boundaries.cjs +9218 -0
- package/dist/boundaries.cjs.map +1 -0
- package/dist/boundaries.d.cts +30 -0
- package/dist/boundaries.d.mts +30 -0
- package/dist/boundaries.mjs +9215 -0
- package/dist/boundaries.mjs.map +1 -0
- package/dist/changes-B-wz9-Gy.cjs +841 -0
- package/dist/changes-B-wz9-Gy.cjs.map +1 -0
- package/dist/changes-CMVZbfZO.d.mts +8 -0
- package/dist/changes-DLZkHBwZ.mjs +800 -0
- package/dist/changes-DLZkHBwZ.mjs.map +1 -0
- package/dist/changes-DnnwRAKl.d.cts +8 -0
- package/dist/components-B7060em0.mjs +240 -0
- package/dist/components-B7060em0.mjs.map +1 -0
- package/dist/components-BuMUB7XT.d.cts +47 -0
- package/dist/components-DZz72pd1.d.mts +47 -0
- package/dist/components-grWmOsaV.cjs +247 -0
- package/dist/components-grWmOsaV.cjs.map +1 -0
- package/dist/components.css +251 -0
- package/dist/data/cities.csv +1253 -0
- package/dist/data/cities.json +23305 -0
- package/dist/data/iran-cities.sql +1487 -0
- package/dist/data/iran-provinces.geojson +1 -0
- package/dist/data/provinces.csv +32 -0
- package/dist/data/provinces.json +460 -0
- package/dist/hooks-C6kjWLIE.mjs +134 -0
- package/dist/hooks-C6kjWLIE.mjs.map +1 -0
- package/dist/hooks-K7pTMF2R.cjs +139 -0
- package/dist/hooks-K7pTMF2R.cjs.map +1 -0
- package/dist/index.cjs +30 -7550
- package/dist/index.d.cts +25 -0
- package/dist/index.d.mts +25 -25
- package/dist/index.mjs +3 -7507
- package/dist/lite-CtNPRhU-.mjs +1926 -0
- package/dist/lite-CtNPRhU-.mjs.map +1 -0
- package/dist/lite-CutarzAc.cjs +2051 -0
- package/dist/lite-CutarzAc.cjs.map +1 -0
- package/dist/lite-react-1HVWJOWt.cjs +55 -0
- package/dist/lite-react-1HVWJOWt.cjs.map +1 -0
- package/dist/lite-react-Ds-DNmEi.mjs +20 -0
- package/dist/lite-react-Ds-DNmEi.mjs.map +1 -0
- package/dist/lite-react-components.cjs +12 -0
- package/dist/lite-react-components.cjs.map +1 -0
- package/dist/lite-react-components.d.cts +5 -0
- package/dist/lite-react-components.d.mts +5 -0
- package/dist/lite-react-components.mjs +11 -0
- package/dist/lite-react-components.mjs.map +1 -0
- package/dist/lite-react.cjs +8 -0
- package/dist/lite-react.d.cts +7 -0
- package/dist/lite-react.d.mts +7 -0
- package/dist/lite-react.mjs +2 -0
- package/dist/lite.cjs +28 -0
- package/dist/lite.d.cts +23 -0
- package/dist/lite.d.mts +23 -0
- package/dist/lite.mjs +3 -0
- package/dist/react-BKmJ476x.mjs +20 -0
- package/dist/react-BKmJ476x.mjs.map +1 -0
- package/dist/react-BZV3TkXB.cjs +55 -0
- package/dist/react-BZV3TkXB.cjs.map +1 -0
- package/dist/react-components.cjs +12 -0
- package/dist/react-components.cjs.map +1 -0
- package/dist/react-components.d.cts +5 -0
- package/dist/react-components.d.mts +5 -0
- package/dist/react-components.mjs +11 -0
- package/dist/react-components.mjs.map +1 -0
- package/dist/react.cjs +8 -7553
- package/dist/react.d.cts +7 -0
- package/dist/react.d.mts +7 -30
- package/dist/react.mjs +2 -7524
- package/dist/rolldown-runtime-78lK1XfQ.cjs +42 -0
- package/dist/rolldown-runtime-D3KUYPsf.mjs +12 -0
- package/dist/src-DRaUVbc4.mjs +4302 -0
- package/dist/src-DRaUVbc4.mjs.map +1 -0
- package/dist/src-mqZTwJfQ.cjs +4439 -0
- package/dist/src-mqZTwJfQ.cjs.map +1 -0
- package/dist/types/boundaries.d.ts +30 -0
- package/dist/types/changes.d.ts +4 -0
- package/dist/types/components.d.ts +47 -0
- package/dist/types/core.d.ts +25 -0
- package/dist/types/geo.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/keyboard.d.ts +3 -0
- package/dist/types/lite-react-components.d.ts +5 -0
- package/dist/types/lite-react.d.ts +7 -0
- package/dist/types/lite.d.ts +26 -0
- package/dist/types/normalize.d.ts +1 -0
- package/dist/types/packed.d.ts +16 -0
- package/dist/types/react-components.d.ts +5 -0
- package/dist/types/react.d.ts +7 -0
- package/dist/types/types.d.ts +149 -0
- package/dist/types/valibot.d.ts +23 -0
- package/dist/types/validation-shared.d.ts +11 -0
- package/dist/types/zod.d.ts +23 -0
- package/dist/types-Ds_b0yHP.d.cts +150 -0
- package/dist/types-Ds_b0yHP.d.mts +150 -0
- package/dist/valibot.cjs +47 -0
- package/dist/valibot.cjs.map +1 -0
- package/dist/valibot.d.cts +21 -0
- package/dist/valibot.d.mts +21 -0
- package/dist/valibot.mjs +38 -0
- package/dist/valibot.mjs.map +1 -0
- package/dist/validation-shared-BFilTuY1.cjs +58 -0
- package/dist/validation-shared-BFilTuY1.cjs.map +1 -0
- package/dist/validation-shared-C7l2cv4e.d.cts +8 -0
- package/dist/validation-shared-CJjGRx8p.d.mts +7 -0
- package/dist/validation-shared-mh_saZne.mjs +29 -0
- package/dist/validation-shared-mh_saZne.mjs.map +1 -0
- package/dist/zod.cjs +49 -0
- package/dist/zod.cjs.map +1 -0
- package/dist/zod.d.cts +21 -0
- package/dist/zod.d.mts +21 -0
- package/dist/zod.mjs +42 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +177 -29
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -25
- package/dist/index.mjs.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.d.ts +0 -30
- package/dist/react.mjs.map +0 -1
- package/dist/types-DzSw8Uw2.d.mts +0 -52
- package/dist/types-DzSw8Uw2.d.ts +0 -52
package/README.md
CHANGED
|
@@ -1,295 +1,618 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English · [فارسی](./README.fa.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# 🇮🇷 Iran Cities
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- ✅ Vue 2 / Vue 3
|
|
9
|
-
- ✅ Angular
|
|
10
|
-
- ✅ Svelte
|
|
11
|
-
- ✅ Node.js / Express (pure backend)
|
|
12
|
-
- ✅ Vanilla JS (browser or any other bundler)
|
|
7
|
+
**Every province and city of Iran, in Persian and English, with fast lookup, smart search, coordinates, boundaries and ready-made components.**
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
Zero dependencies · Tree-shakeable · Fully typed · Built for Next.js
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/@code-plate/iran-cities)
|
|
12
|
+
[](https://www.npmjs.com/package/@code-plate/iran-cities)
|
|
13
|
+
[](https://github.com/MJavadSF/Iran-Cities/actions/workflows/ci.yml)
|
|
14
|
+
[](https://scorecard.dev/viewer/?uri=github.com/MJavadSF/Iran-Cities)
|
|
15
|
+
[](#typescript)
|
|
16
|
+
|
|
17
|
+
**[Documentation](https://mjavadsf.github.io/Iran-Cities/)** · **[Live playground](https://mjavadsf.github.io/Iran-Cities/playground/)** · **[Map](https://mjavadsf.github.io/Iran-Cities/guides/geography/)**
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { getCity, searchCities, getNeighborProvinces } from '@code-plate/iran-cities';
|
|
23
|
+
|
|
24
|
+
getCity('شيراز'); // { id: 292, fa: 'شیراز', nameEn: 'Shiraz', provinceFa: 'فارس', phoneCode: '071', ... }
|
|
25
|
+
searchCities('jivhk'); // [تهران] typed on an English keyboard
|
|
26
|
+
getNeighborProvinces('قم'); // [اصفهان, مرکزی, سمنان, تهران]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
<table>
|
|
30
|
+
<tr>
|
|
31
|
+
<td width="33%" valign="top">
|
|
32
|
+
|
|
33
|
+
### 📍 Complete
|
|
34
|
+
**31** provinces and **1,252** cities with stable ids, Persian and English names, area codes, capitals, neighboring provinces and **1,152** coordinates, all checked against province boundaries.
|
|
35
|
+
|
|
36
|
+
</td>
|
|
37
|
+
<td width="33%" valign="top">
|
|
38
|
+
|
|
39
|
+
### 🔎 Persian-aware
|
|
40
|
+
Understands `ي`/`ی`, `ك`/`ک`, `آ`/`ا`, ZWNJ, diacritics and Persian digits, tolerates typos, and fixes text typed with the wrong keyboard layout.
|
|
41
|
+
|
|
42
|
+
</td>
|
|
43
|
+
<td width="33%" valign="top">
|
|
44
|
+
|
|
45
|
+
### ⚡ Ready for production
|
|
46
|
+
Accessible components in plain CSS, Zod and Valibot schemas, `"use client"` hooks, a `lite` build, frozen data, and types from TypeScript 3.9 to 7.
|
|
47
|
+
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
</table>
|
|
51
|
+
|
|
52
|
+
> [!TIP]
|
|
53
|
+
> Upgrading from 1.x? Most projects need no changes. See the [migration guide](./MIGRATION.md).
|
|
54
|
+
|
|
55
|
+
## Contents
|
|
56
|
+
|
|
57
|
+
- [Installation](#installation)
|
|
58
|
+
- [Entry points](#entry-points)
|
|
59
|
+
- [Quick start](#quick-start)
|
|
60
|
+
- [Next.js](#nextjs)
|
|
61
|
+
- [Components](#components)
|
|
62
|
+
- [Form validation](#form-validation)
|
|
63
|
+
- [React hooks](#react-hooks)
|
|
64
|
+
- [Maps and geography](#maps-and-geography)
|
|
65
|
+
- [Vue, Svelte, Angular, Express](#vue-svelte-angular-express)
|
|
66
|
+
- [Other languages: CSV, SQL, JSON](#other-languages-csv-sql-json)
|
|
67
|
+
- [API reference](#api-reference)
|
|
68
|
+
- [How matching works](#how-matching-works)
|
|
69
|
+
- [Data and sources](#data-and-sources)
|
|
70
|
+
- [Compatibility](#compatibility)
|
|
71
|
+
- [Contributing](#contributing)
|
|
72
|
+
- [License and credits](#license-and-credits)
|
|
15
73
|
|
|
16
74
|
## Installation
|
|
17
75
|
|
|
18
76
|
```bash
|
|
19
77
|
npm install @code-plate/iran-cities
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary>pnpm, yarn, bun, Deno</summary>
|
|
82
|
+
|
|
83
|
+
```bash
|
|
23
84
|
pnpm add @code-plate/iran-cities
|
|
85
|
+
yarn add @code-plate/iran-cities
|
|
86
|
+
bun add @code-plate/iran-cities
|
|
87
|
+
deno add npm:@code-plate/iran-cities
|
|
24
88
|
```
|
|
25
89
|
|
|
26
|
-
|
|
90
|
+
</details>
|
|
91
|
+
|
|
92
|
+
## Entry points
|
|
27
93
|
|
|
28
|
-
-
|
|
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.
|
|
94
|
+
Pick the smallest entry that has what you need. Sizes are minified and brotli-compressed, data included.
|
|
33
95
|
|
|
34
|
-
|
|
96
|
+
| Import | What you get | Size |
|
|
97
|
+
|---|---|---:|
|
|
98
|
+
| `@code-plate/iran-cities` | Every function, including coordinates and distances | 24 KB |
|
|
99
|
+
| `@code-plate/iran-cities/lite` | The same lookups and search, without coordinates | 16 KB |
|
|
100
|
+
| `@code-plate/iran-cities/react` | React hooks | 25 KB |
|
|
101
|
+
| `@code-plate/iran-cities/lite/react` | React hooks without coordinates | 17 KB |
|
|
102
|
+
| `@code-plate/iran-cities/lite/react/components` | `CityCombobox` and `ProvinceCitySelect` | 18 KB |
|
|
103
|
+
| `@code-plate/iran-cities/react/components` | The same components with coordinates | 26 KB |
|
|
104
|
+
| `@code-plate/iran-cities/components.css` | The components' stylesheet | 1.5 KB |
|
|
105
|
+
| `@code-plate/iran-cities/zod` · `/valibot` | Validation schemas | 16 KB |
|
|
106
|
+
| `@code-plate/iran-cities/boundaries` | Province boundaries (GeoJSON) and `getProvinceAt` | 53 KB |
|
|
107
|
+
| `@code-plate/iran-cities/data/*` | CSV, SQL, JSON and GeoJSON files | |
|
|
35
108
|
|
|
36
|
-
|
|
109
|
+
Entries share code, so importing several never ships the data twice. React, Zod and Valibot are optional peer dependencies.
|
|
110
|
+
|
|
111
|
+
## Quick start
|
|
112
|
+
|
|
113
|
+
### Look things up
|
|
37
114
|
|
|
38
115
|
```ts
|
|
39
|
-
import {
|
|
40
|
-
getProvinces,
|
|
41
|
-
getProvincesList,
|
|
42
|
-
getProvince,
|
|
43
|
-
getCities,
|
|
44
|
-
getCity,
|
|
45
|
-
getCityById,
|
|
46
|
-
getProvinceById,
|
|
47
|
-
getCitiesByName,
|
|
48
|
-
getAllCities,
|
|
49
|
-
getProvinceOfCity,
|
|
50
|
-
searchProvinces,
|
|
51
|
-
searchCities,
|
|
52
|
-
getStats,
|
|
53
|
-
} from '@code-plate/iran-cities';
|
|
54
|
-
|
|
55
|
-
// All provinces along with their cities
|
|
56
|
-
getProvinces();
|
|
57
|
-
// [{ id: 1, en: 'alborz', fa: 'البرز', cities: [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj' }, ...] }, ...]
|
|
58
|
-
|
|
59
|
-
// Just the list of provinces (no cities) - for a Select/Dropdown
|
|
60
|
-
getProvincesList();
|
|
61
|
-
// [{ id: 1, en: 'alborz', fa: 'البرز' }, { id: 2, en: 'ardabil', fa: 'اردبیل' }, ...]
|
|
62
|
-
|
|
63
|
-
// Get a single province by English id or Persian name
|
|
64
|
-
getProvince('tehran');
|
|
65
|
-
getProvince('تهران'); // same result
|
|
66
|
-
getProvince(28); // numeric id works too (so does the string '28')
|
|
67
|
-
|
|
68
|
-
// Cities of a specific province
|
|
69
|
-
getCities('fars'); // by English id
|
|
70
|
-
getCities('فارس'); // or by Persian name, both work the same way
|
|
71
|
-
|
|
72
|
-
// Find a specific city (across the whole country, or scoped to a province)
|
|
73
|
-
getCity('shiraz');
|
|
74
|
-
getCity('شیراز', 'fars'); // scoped to Fars province
|
|
75
|
-
|
|
76
|
-
// Flat list of every city in the country, along with its province
|
|
77
|
-
getAllCities();
|
|
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)]
|
|
86
|
-
|
|
87
|
-
// Find which province a city belongs to
|
|
88
|
-
getProvinceOfCity('اصفهان'); // => { id: 6, en: 'isfahan', fa: 'اصفهان', cities: [...] }
|
|
89
|
-
getProvinceOfCity(292); // city id works too
|
|
90
|
-
|
|
91
|
-
// Live search (autocomplete) over provinces or cities
|
|
92
|
-
searchProvinces('خراسان'); // returns all three "Khorasan" provinces
|
|
93
|
-
searchCities('شهر', { limit: 10 }); // first 10 results containing "شهر"
|
|
94
|
-
searchCities('mashhad', { locale: 'en' }); // search English names only
|
|
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
|
-
|
|
103
|
-
// Overall stats
|
|
104
|
-
getStats(); // { provinceCount: 31, cityCount: 1252 }
|
|
105
|
-
```
|
|
116
|
+
import { getProvince, getCities, getCity, getCityById, getProvinceOfCity } from '@code-plate/iran-cities';
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
getProvince('tehran'); // by English id
|
|
119
|
+
getProvince('تهران'); // by Persian name
|
|
120
|
+
getProvince(28); // by id ('28' and '۲۸' work too)
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
getCities('fars'); // all cities of Fars
|
|
123
|
+
getCity('شیراز'); // a city anywhere in Iran
|
|
124
|
+
getCity('شیراز', 'fars'); // a city within a province
|
|
125
|
+
getCityById(292); // by its global id
|
|
126
|
+
getProvinceOfCity('اصفهان'); // the province a city belongs to
|
|
127
|
+
```
|
|
111
128
|
|
|
112
|
-
|
|
129
|
+
### Search and autocomplete
|
|
113
130
|
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
const { getProvincesList, getCities, searchCities } = require('@code-plate/iran-cities');
|
|
131
|
+
```ts
|
|
132
|
+
import { searchCities, searchProvinces } from '@code-plate/iran-cities';
|
|
117
133
|
|
|
118
|
-
|
|
134
|
+
searchProvinces('خراسان'); // [خراسان جنوبی, خراسان رضوی, خراسان شمالی]
|
|
135
|
+
searchCities('شهر', { limit: 10 }); // first 10 matches
|
|
136
|
+
searchCities('تهرن', { fuzzy: true }); // typo tolerant: [تهران]
|
|
137
|
+
searchCities('jivhk'); // wrong keyboard layout: [تهران]
|
|
138
|
+
searchCities('', { emptyQuery: 'none' }); // [] instead of all 1,252 cities
|
|
139
|
+
```
|
|
119
140
|
|
|
120
|
-
|
|
121
|
-
res.json(getProvincesList());
|
|
122
|
-
});
|
|
141
|
+
### Neighbors, capitals and area codes
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
143
|
+
```ts
|
|
144
|
+
import { getNeighborProvinces, areNeighborProvinces, getCapital, getProvinceByPhoneCode } from '@code-plate/iran-cities';
|
|
127
145
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
getNeighborProvinces('قم'); // [اصفهان, مرکزی, سمنان, تهران]
|
|
147
|
+
areNeighborProvinces('yazd', 'south-khorasan'); // true
|
|
148
|
+
getCapital('khuzestan'); // اهواز
|
|
149
|
+
getProvinceByPhoneCode('021'); // تهران ('21', 21, '۰۲۱', '+98 21' work too)
|
|
131
150
|
```
|
|
132
151
|
|
|
133
|
-
|
|
152
|
+
### English display names
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
getProvince(16)?.nameEn; // 'Kohgiluyeh and Boyer-Ahmad' (the id `en` is 'kohgiluyeh-and-boyer-ahmad')
|
|
156
|
+
getCity('bandar-e-gaz')?.nameEn; // 'Bandar-e Gaz'
|
|
157
|
+
```
|
|
134
158
|
|
|
135
|
-
##
|
|
159
|
+
## Next.js
|
|
136
160
|
|
|
137
|
-
|
|
161
|
+
Use the core entry in **Server Components** and the hooks or components in **Client Components**. Every React entry already declares `"use client"`.
|
|
138
162
|
|
|
139
163
|
```tsx
|
|
140
|
-
|
|
164
|
+
// app/address/page.tsx (Server Component)
|
|
165
|
+
import { getCity } from '@code-plate/iran-cities';
|
|
166
|
+
import AddressForm from './address-form';
|
|
167
|
+
import { save } from './actions';
|
|
168
|
+
|
|
169
|
+
export default function Page() {
|
|
170
|
+
const city = getCity('شیراز', 'fars');
|
|
171
|
+
return <AddressForm action={save} defaultCityId={city?.id} />;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
141
174
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
```tsx
|
|
176
|
+
// app/address/address-form.tsx (Client Component)
|
|
177
|
+
'use client';
|
|
145
178
|
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
import '@code-plate/iran-cities/components.css';
|
|
180
|
+
import { ProvinceCitySelect } from '@code-plate/iran-cities/lite/react/components';
|
|
148
181
|
|
|
182
|
+
export default function AddressForm({ action, defaultCityId }: { action: (data: FormData) => void; defaultCityId?: number }) {
|
|
149
183
|
return (
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<option key={p.id} value={p.en}>{p.fa}</option>
|
|
155
|
-
))}
|
|
156
|
-
</select>
|
|
157
|
-
|
|
158
|
-
<select value={city} onChange={(e) => setCity(e.target.value)} disabled={!province}>
|
|
159
|
-
<option value="">Select a city</option>
|
|
160
|
-
{cities.map((c) => (
|
|
161
|
-
<option key={c.id} value={c.en}>{c.fa}</option>
|
|
162
|
-
))}
|
|
163
|
-
</select>
|
|
164
|
-
</>
|
|
184
|
+
<form action={action}>
|
|
185
|
+
<ProvinceCitySelect provinceName="provinceId" cityName="cityId" defaultValue={{ cityId: defaultCityId }} required />
|
|
186
|
+
<button type="submit">ثبت</button>
|
|
187
|
+
</form>
|
|
165
188
|
);
|
|
166
189
|
}
|
|
167
190
|
```
|
|
168
191
|
|
|
169
|
-
|
|
192
|
+
```ts
|
|
193
|
+
// app/address/actions.ts
|
|
194
|
+
'use server';
|
|
195
|
+
|
|
196
|
+
import { provinceCitySchema } from '@code-plate/iran-cities/zod';
|
|
197
|
+
|
|
198
|
+
export async function save(formData: FormData) {
|
|
199
|
+
const result = provinceCitySchema.safeParse({ provinceId: formData.get('provinceId'), cityId: formData.get('cityId') });
|
|
200
|
+
if (!result.success) return;
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
> [!TIP]
|
|
205
|
+
> Use the `lite` entries in Client Components unless the browser really needs coordinates. That keeps about 8 KB out of the client bundle.
|
|
206
|
+
|
|
207
|
+
Tested with Next.js 14, 15 and 16, with Turbopack and webpack.
|
|
208
|
+
|
|
209
|
+
## Components
|
|
210
|
+
|
|
211
|
+
Two accessible components, styled with one small plain-CSS file (no Tailwind, no CSS-in-JS). [See them live](https://mjavadsf.github.io/Iran-Cities/guides/components/).
|
|
170
212
|
|
|
171
213
|
```tsx
|
|
172
|
-
|
|
214
|
+
'use client';
|
|
173
215
|
|
|
174
|
-
|
|
175
|
-
|
|
216
|
+
import '@code-plate/iran-cities/components.css';
|
|
217
|
+
import { CityCombobox, ProvinceCitySelect } from '@code-plate/iran-cities/lite/react/components';
|
|
176
218
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
<ProvinceCitySelect provinceName="provinceId" cityName="cityId" onChange={({ provinceId, cityId }) => {}} />
|
|
220
|
+
<CityCombobox label="شهر مقصد" name="destinationId" province="fars" onChange={(city) => {}} />
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- **`CityCombobox`** follows the [WAI-ARIA combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/): `↑` `↓`, `Home` `End`, `Enter` to select, `Escape` to close and again to clear. Typo tolerant and fixes the keyboard layout.
|
|
224
|
+
- **`ProvinceCitySelect`** is two labelled native `<select>`s; the city resets when the province changes.
|
|
225
|
+
- Both submit **ids** through normal form fields (`name`), so they work with plain forms and Server Actions. Both can be controlled (`value` + `onChange`) or uncontrolled (`defaultValue`), so they plug into react-hook-form's `Controller` as-is.
|
|
226
|
+
- Right-to-left by default (`dir="rtl"`), English names with `locale="en"`, automatic dark mode.
|
|
227
|
+
|
|
228
|
+
Theme with CSS variables on any parent. All selectors use `:where()`, so your own classes win without `!important`:
|
|
229
|
+
|
|
230
|
+
```css
|
|
231
|
+
.checkout {
|
|
232
|
+
--ic-accent: #2563eb;
|
|
233
|
+
--ic-radius: 0.375rem;
|
|
234
|
+
--ic-height: 2.5rem;
|
|
235
|
+
--ic-font: 'Vazirmatn', sans-serif;
|
|
187
236
|
}
|
|
188
237
|
```
|
|
189
238
|
|
|
190
|
-
|
|
239
|
+
<details>
|
|
240
|
+
<summary>All CSS variables</summary>
|
|
191
241
|
|
|
192
|
-
|
|
|
242
|
+
| Variable | |
|
|
193
243
|
|---|---|
|
|
194
|
-
| `
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
244
|
+
| `--ic-font`, `--ic-font-size`, `--ic-line-height` | Typography |
|
|
245
|
+
| `--ic-height`, `--ic-radius`, `--ic-gap`, `--ic-padding-inline` | Size and spacing |
|
|
246
|
+
| `--ic-bg`, `--ic-fg`, `--ic-muted`, `--ic-border`, `--ic-border-hover` | Colors |
|
|
247
|
+
| `--ic-accent`, `--ic-accent-soft`, `--ic-ring` | Accent and focus ring |
|
|
248
|
+
| `--ic-danger`, `--ic-danger-ring` | Invalid state |
|
|
249
|
+
| `--ic-popover-bg`, `--ic-shadow`, `--ic-listbox-max-height` | Suggestion list |
|
|
250
|
+
| `--ic-disabled-bg`, `--ic-transition` | Other |
|
|
251
|
+
|
|
252
|
+
Dark mode follows `prefers-color-scheme`; force it with `data-theme="dark"` or `"light"` on any parent.
|
|
253
|
+
|
|
254
|
+
</details>
|
|
255
|
+
|
|
256
|
+
## Form validation
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { provinceCitySchema, cityIdSchema, citySchema } from '@code-plate/iran-cities/zod';
|
|
260
|
+
|
|
261
|
+
provinceCitySchema.parse({ provinceId: '7', cityId: '292' }); // { provinceId: 7, cityId: 292 }
|
|
262
|
+
provinceCitySchema.safeParse({ provinceId: 28, cityId: 292 }); // error on cityId: «این شهر در استان انتخابشده نیست.»
|
|
263
|
+
cityIdSchema.parse('۲۹۲'); // 292
|
|
264
|
+
citySchema.parse('شیراز'); // the full city object
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The same schemas exist for Valibot in `@code-plate/iran-cities/valibot`. Messages are Persian by default; pass your own with `createZodSchemas({ ... })` or `createValibotSchemas({ ... })`.
|
|
268
|
+
|
|
269
|
+
## React hooks
|
|
270
|
+
|
|
271
|
+
Import from `@code-plate/iran-cities/react`, or `@code-plate/iran-cities/lite/react` without coordinates.
|
|
272
|
+
|
|
273
|
+
| Hook | Returns |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `useProvinces()` | The list of provinces |
|
|
276
|
+
| `useCities(province)` | Cities of a province |
|
|
277
|
+
| `useProvinceCitySelect(options?)` | Everything a province → city field needs; options `initialProvince`, `initialCity`, `value`, `onChange` |
|
|
278
|
+
| `useCitySearch(initialQuery?, options?)` | `{ query, setQuery, results }` |
|
|
279
|
+
| `useProvinceSearch(initialQuery?, options?)` | `{ query, setQuery, results }` |
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
const { provinces, cities, province, city, setProvince, setCity, provinceId, cityId } = useProvinceCitySelect({ initialCity: 292 });
|
|
283
|
+
const [value, setValue] = useState<ProvinceCityValue>({ provinceId: 7, cityId: 292 });
|
|
284
|
+
useProvinceCitySelect({ value, onChange: setValue }); // controlled
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Maps and geography
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { getDistance, getNearestCities } from '@code-plate/iran-cities';
|
|
291
|
+
import { getProvinceAt, provinceBoundaries } from '@code-plate/iran-cities/boundaries';
|
|
292
|
+
|
|
293
|
+
getDistance('tehran', 'karaj'); // 36.5 (km)
|
|
294
|
+
getNearestCities({ lat: 29.6, lng: 52.5 }, { limit: 1 }); // [{ city: شیراز, distanceKm: 5.6 }]
|
|
295
|
+
getProvinceAt({ lat: 34.64, lng: 50.88 }); // قم
|
|
296
|
+
getProvinceAt({ lat: 48.85, lng: 2.35 }); // undefined (outside Iran)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`provinceBoundaries` is a standard GeoJSON `FeatureCollection` that works directly with Leaflet, MapLibre and deck.gl. Every city with coordinates resolves to its own province. [Try the map](https://mjavadsf.github.io/Iran-Cities/guides/geography/).
|
|
199
300
|
|
|
200
|
-
|
|
301
|
+
## Vue, Svelte, Angular, Express
|
|
201
302
|
|
|
202
|
-
|
|
303
|
+
The core functions are plain synchronous functions, so they work in any framework.
|
|
203
304
|
|
|
204
|
-
|
|
305
|
+
<details>
|
|
306
|
+
<summary><b>Vue 3</b></summary>
|
|
205
307
|
|
|
206
308
|
```vue
|
|
207
309
|
<script setup>
|
|
208
310
|
import { ref, computed } from 'vue';
|
|
209
|
-
import { getProvincesList, getCities } from '@code-plate/iran-cities';
|
|
311
|
+
import { getProvincesList, getCities } from '@code-plate/iran-cities/lite';
|
|
210
312
|
|
|
211
313
|
const provinces = getProvincesList();
|
|
212
|
-
const
|
|
213
|
-
const cities = computed(() => getCities(
|
|
314
|
+
const province = ref('');
|
|
315
|
+
const cities = computed(() => getCities(province.value));
|
|
214
316
|
</script>
|
|
215
317
|
|
|
216
318
|
<template>
|
|
217
|
-
<select v-model="
|
|
218
|
-
<option v-for="p in provinces" :key="p.
|
|
319
|
+
<select v-model="province">
|
|
320
|
+
<option v-for="p in provinces" :key="p.id" :value="p.en">{{ p.fa }}</option>
|
|
219
321
|
</select>
|
|
220
|
-
<select :disabled="!
|
|
221
|
-
<option v-for="c in cities" :key="c.
|
|
322
|
+
<select :disabled="!province">
|
|
323
|
+
<option v-for="c in cities" :key="c.id" :value="c.en">{{ c.fa }}</option>
|
|
222
324
|
</select>
|
|
223
325
|
</template>
|
|
224
326
|
```
|
|
225
327
|
|
|
226
|
-
|
|
328
|
+
</details>
|
|
329
|
+
|
|
330
|
+
<details>
|
|
331
|
+
<summary><b>Svelte</b></summary>
|
|
332
|
+
|
|
333
|
+
```svelte
|
|
334
|
+
<script>
|
|
335
|
+
import { getProvincesList, getCities } from '@code-plate/iran-cities/lite';
|
|
336
|
+
|
|
337
|
+
const provinces = getProvincesList();
|
|
338
|
+
let province = '';
|
|
339
|
+
$: cities = getCities(province);
|
|
340
|
+
</script>
|
|
341
|
+
|
|
342
|
+
<select bind:value={province}>
|
|
343
|
+
{#each provinces as p (p.id)}<option value={p.en}>{p.fa}</option>{/each}
|
|
344
|
+
</select>
|
|
345
|
+
<select disabled={!province}>
|
|
346
|
+
{#each cities as c (c.id)}<option value={c.en}>{c.fa}</option>{/each}
|
|
347
|
+
</select>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
</details>
|
|
351
|
+
|
|
352
|
+
<details>
|
|
353
|
+
<summary><b>Angular</b></summary>
|
|
227
354
|
|
|
228
355
|
```ts
|
|
229
356
|
import { Injectable } from '@angular/core';
|
|
230
|
-
import { getProvincesList, getCities, type City } from '@code-plate/iran-cities';
|
|
357
|
+
import { getProvincesList, getCities, type City, type ProvinceListItem } from '@code-plate/iran-cities/lite';
|
|
231
358
|
|
|
232
359
|
@Injectable({ providedIn: 'root' })
|
|
233
360
|
export class IranLocationsService {
|
|
234
|
-
|
|
361
|
+
provinces(): ProvinceListItem[] {
|
|
235
362
|
return getProvincesList();
|
|
236
363
|
}
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
|
|
365
|
+
citiesOf(province: string): readonly City[] {
|
|
366
|
+
return getCities(province);
|
|
239
367
|
}
|
|
240
368
|
}
|
|
241
369
|
```
|
|
242
370
|
|
|
243
|
-
|
|
371
|
+
</details>
|
|
372
|
+
|
|
373
|
+
<details>
|
|
374
|
+
<summary><b>Node.js / Express</b></summary>
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
const express = require('express');
|
|
378
|
+
const { getProvincesList, getCities, searchCities } = require('@code-plate/iran-cities');
|
|
379
|
+
|
|
380
|
+
const app = express();
|
|
381
|
+
|
|
382
|
+
app.get('/api/provinces', (req, res) => res.json(getProvincesList()));
|
|
383
|
+
app.get('/api/cities/search', (req, res) => res.json(searchCities(String(req.query.q ?? ''), { limit: 20 })));
|
|
384
|
+
app.get('/api/cities/:province', (req, res) => res.json(getCities(req.params.province)));
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The search route is registered before `/:province`, otherwise Express would treat `search` as a province name.
|
|
388
|
+
|
|
389
|
+
</details>
|
|
390
|
+
|
|
391
|
+
## Other languages: CSV, SQL, JSON
|
|
392
|
+
|
|
393
|
+
The package also ships the data as plain files for Laravel, Django, Go or any database:
|
|
394
|
+
|
|
395
|
+
| File | |
|
|
396
|
+
|---|---|
|
|
397
|
+
| `data/provinces.csv`, `data/cities.csv` | UTF-8 CSV with a header row |
|
|
398
|
+
| `data/provinces.json`, `data/cities.json` | Flat JSON arrays |
|
|
399
|
+
| `data/iran-cities.sql` | Tables, foreign keys and inserts for PostgreSQL, MySQL, MariaDB and SQLite |
|
|
400
|
+
| `data/iran-provinces.geojson` | Province boundaries |
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
curl -O https://unpkg.com/@code-plate/iran-cities/dist/data/iran-cities.sql
|
|
404
|
+
psql mydb < iran-cities.sql
|
|
405
|
+
```
|
|
244
406
|
|
|
245
|
-
##
|
|
407
|
+
## API reference
|
|
246
408
|
|
|
247
|
-
|
|
409
|
+
Wherever a query is expected, you can pass a Persian name, an English id, an English display name or a numeric id (number or string, Persian digits included).
|
|
410
|
+
|
|
411
|
+
<details open>
|
|
412
|
+
<summary><b>Provinces</b></summary>
|
|
413
|
+
|
|
414
|
+
| Function | Returns |
|
|
415
|
+
|---|---|
|
|
416
|
+
| `getProvinces()` | `readonly Province[]` with their cities |
|
|
417
|
+
| `getProvincesList()` | `ProvinceListItem[]` without cities (a new array every call) |
|
|
418
|
+
| `getProvince(query)` | `Province \| undefined` |
|
|
419
|
+
| `getProvinceById(id)` | `Province \| undefined` |
|
|
420
|
+
| `getProvinceByPhoneCode(code)` | `Province \| undefined` |
|
|
421
|
+
| `getProvinceOfCity(cityQuery)` | `Province \| undefined` |
|
|
422
|
+
| `getNeighborProvinces(query)` | `Province[]` that share a border |
|
|
423
|
+
| `areNeighborProvinces(a, b)` | `boolean` |
|
|
424
|
+
|
|
425
|
+
</details>
|
|
426
|
+
|
|
427
|
+
<details open>
|
|
428
|
+
<summary><b>Cities</b></summary>
|
|
429
|
+
|
|
430
|
+
| Function | Returns |
|
|
431
|
+
|---|---|
|
|
432
|
+
| `getCities(provinceQuery)` | `readonly City[]` |
|
|
433
|
+
| `getAllCities()` | `readonly FlatCity[]` |
|
|
434
|
+
| `getCity(cityQuery, provinceQuery?)` | `FlatCity \| undefined` |
|
|
435
|
+
| `getCityById(id)` | `FlatCity \| undefined` |
|
|
436
|
+
| `getCityBySlug(slug)` | `FlatCity \| undefined`, e.g. `'fars/shiraz'`; old slugs still resolve |
|
|
437
|
+
| `getCitiesByName(name)` | `readonly FlatCity[]`, every city with that name or alias |
|
|
438
|
+
| `isAmbiguousCityName(name)` | `boolean` |
|
|
439
|
+
| `getCapital(provinceQuery)` | `FlatCity \| undefined` |
|
|
440
|
+
| `getCapitals()` | `FlatCity[]`, the 31 provincial capitals |
|
|
441
|
+
|
|
442
|
+
</details>
|
|
443
|
+
|
|
444
|
+
<details open>
|
|
445
|
+
<summary><b>Search</b></summary>
|
|
446
|
+
|
|
447
|
+
| Function | Returns |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `searchCities(query, options?)` | `FlatCity[]` |
|
|
450
|
+
| `searchProvinces(query, options?)` | `Province[]` |
|
|
451
|
+
| `normalize(text)` | `string`, the normalization every lookup uses |
|
|
452
|
+
| `convertKeyboardLayout(text, 'fa' \| 'en')` | `string` |
|
|
453
|
+
|
|
454
|
+
| `SearchOptions` | Default | |
|
|
455
|
+
|---|---|---|
|
|
456
|
+
| `limit` | no limit | Maximum number of results |
|
|
457
|
+
| `locale` | `'both'` | `'fa'`, `'en'` or `'both'` |
|
|
458
|
+
| `fuzzy` | `false` | Tolerate typos |
|
|
459
|
+
| `maxDistance` | `2` | Maximum edit distance with `fuzzy`; capped at a third of the query length |
|
|
460
|
+
| `emptyQuery` | `'all'` | What an empty query returns: `'all'` or `'none'` |
|
|
461
|
+
| `keyboardFix` | `true` | When nothing matches, retry with the other keyboard layout |
|
|
462
|
+
|
|
463
|
+
</details>
|
|
464
|
+
|
|
465
|
+
<details open>
|
|
466
|
+
<summary><b>Geography</b></summary>
|
|
467
|
+
|
|
468
|
+
| Function | Returns |
|
|
469
|
+
|---|---|
|
|
470
|
+
| `getDistance(from, to)` | km, or `undefined` if either side has no coordinates |
|
|
471
|
+
| `getNearestCities(point, options?)` | `{ city, distanceKm }[]`, closest first; options `limit` (5, `0` for all), `minConfidence` (`'low'`), `maxDistanceKm` |
|
|
472
|
+
| `getProvinceAt(point, options?)` | `Province \| undefined`; points within `maxDistanceKm` (5) of a border are matched to the nearest province *(boundaries entry)* |
|
|
473
|
+
| `getProvinceBoundary(id)` | One GeoJSON feature *(boundaries entry)* |
|
|
474
|
+
| `provinceBoundaries` | GeoJSON `FeatureCollection` *(boundaries entry)* |
|
|
248
475
|
|
|
249
|
-
|
|
250
|
-
|---|---|---|---|
|
|
251
|
-
| `getProvinces()` | - | `Province[]` | All provinces with their cities |
|
|
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 |
|
|
262
|
-
| `getAllCities()` | - | `FlatCity[]` | All cities, flattened |
|
|
263
|
-
| `getProvinceOfCity(cityQuery)` | `string \| number` | `Province \| undefined` | The province a city belongs to |
|
|
264
|
-
| `searchProvinces(query, options?)` | `string, SearchOptions?` | `Province[]` | Fuzzy province search |
|
|
265
|
-
| `searchCities(query, options?)` | `string, SearchOptions?` | `FlatCity[]` | Fuzzy city search |
|
|
266
|
-
| `getStats()` | - | `{provinceCount, cityCount}` | Overall stats |
|
|
476
|
+
`from`, `to` and `point` can be a city query or `{ lat, lng }`.
|
|
267
477
|
|
|
268
|
-
|
|
478
|
+
</details>
|
|
269
479
|
|
|
270
|
-
|
|
480
|
+
<details open>
|
|
481
|
+
<summary><b>Data versioning</b></summary>
|
|
482
|
+
|
|
483
|
+
| Export | |
|
|
484
|
+
|---|---|
|
|
485
|
+
| `DATA_VERSION` | The data version, e.g. `'2.0.0'` |
|
|
486
|
+
| `DATA_CHANGES` | Every rename, spelling fix or removal since 1.0 |
|
|
487
|
+
| `getDataChanges(sinceVersion?)` | Changes after a version, to update values you stored |
|
|
488
|
+
|
|
489
|
+
</details>
|
|
490
|
+
|
|
491
|
+
### TypeScript
|
|
271
492
|
|
|
272
493
|
```ts
|
|
273
|
-
interface
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
494
|
+
interface Province {
|
|
495
|
+
id: number; // 1 – 31
|
|
496
|
+
en: string; // 'fars'
|
|
497
|
+
fa: string; // 'فارس'
|
|
498
|
+
nameEn: string; // 'Fars'
|
|
499
|
+
phoneCode: string; // '071'
|
|
500
|
+
capitalId: number;
|
|
501
|
+
neighborIds: readonly number[];
|
|
502
|
+
cities: readonly City[];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
interface City {
|
|
506
|
+
id: number; // unique across Iran, never reused
|
|
507
|
+
en: string; // 'shiraz'
|
|
508
|
+
fa: string; // 'شیراز'
|
|
509
|
+
nameEn: string; // 'Shiraz'
|
|
510
|
+
slug: string; // 'fars/shiraz'
|
|
511
|
+
isCapital: boolean;
|
|
512
|
+
aliases: readonly string[];
|
|
513
|
+
coordinates?: { lat: number; lng: number; confidence: 'high' | 'medium' | 'low' };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
interface FlatCity extends City {
|
|
517
|
+
provinceId: number;
|
|
518
|
+
provinceEn: string;
|
|
519
|
+
provinceFa: string;
|
|
520
|
+
provinceNameEn: string;
|
|
521
|
+
phoneCode: string;
|
|
522
|
+
}
|
|
277
523
|
```
|
|
278
524
|
|
|
279
|
-
|
|
525
|
+
Every type is exported, including `ProvinceListItem`, `SearchOptions`, `NearestCity`, `LatLng`, `ProvinceCityValue`, `CityComboboxProps`, `ProvinceCitySelectProps`, `DataChange` and the API interfaces `IranCities`, `IranCitiesGeo` and `IranCitiesHooks`.
|
|
526
|
+
|
|
527
|
+
> [!NOTE]
|
|
528
|
+
> Provinces, cities and the shared arrays are frozen with `Object.freeze`, so one part of your app can't accidentally change the data for another. Copy before changing: `[...getCities('fars')].sort(...)`. `getProvincesList()`, `getCapitals()`, `getNeighborProvinces()` and the search functions already return new arrays.
|
|
529
|
+
|
|
530
|
+
## How matching works
|
|
531
|
+
|
|
532
|
+
**Normalization.** Every lookup and search compares normalized text, so these are treated as equal:
|
|
533
|
+
|
|
534
|
+
| Treated as the same | |
|
|
535
|
+
|---|---|
|
|
536
|
+
| `ی` `ي` `ى` `ئ` | `ک` `ك` |
|
|
537
|
+
| `ا` `آ` `أ` `إ` `ٱ` | `ه` `ة` `ۀ` |
|
|
538
|
+
| `و` `ؤ` | `۰۱۲` `٠١٢` `012` |
|
|
539
|
+
|
|
540
|
+
Diacritics, tatweel (`ـ`), zero-width non-joiners, spaces, `-`, `_` and apostrophes are ignored, and case doesn't matter.
|
|
541
|
+
|
|
542
|
+
**Keyboard layout.** When a query finds nothing, it's retried as if it had been typed on the other keyboard layout: `jivhk` → `تهران`, `ساهقشظ` → `shiraz`. Turn this off with `{ keyboardFix: false }`.
|
|
543
|
+
|
|
544
|
+
**Ranking.** Exact match first, then prefix, then substring, then (with `fuzzy`) edit distance. Equal scores put provincial capitals first, then Persian alphabetical order.
|
|
545
|
+
|
|
546
|
+
**Shared names.** 25 city names exist in more than one province. Without a province, `getCity(name)` returns the provincial capital, otherwise the better-known city, otherwise the lowest id. When it matters, check `isAmbiguousCityName(name)`, then pass the province or let the user choose from `getCitiesByName(name)`.
|
|
547
|
+
|
|
548
|
+
**Aliases.** Alternative spellings and old names resolve to the right city: `طهران`, `teheran`, `esfahan`, `mashad`, `orumiyeh`, `صومعه سارا`, `حلاشی`.
|
|
280
549
|
|
|
281
|
-
##
|
|
550
|
+
## Data and sources
|
|
551
|
+
|
|
552
|
+
| | |
|
|
553
|
+
|---|---:|
|
|
554
|
+
| Provinces | 31 |
|
|
555
|
+
| Cities | 1,252 |
|
|
556
|
+
| Cities with coordinates | 1,152 |
|
|
557
|
+
| ↳ `high` confidence | 977 |
|
|
558
|
+
| ↳ `medium` confidence | 98 |
|
|
559
|
+
| ↳ `low` confidence | 77 |
|
|
560
|
+
| Province borders | 74 |
|
|
561
|
+
|
|
562
|
+
- **`high`**: an independent source confirms the same point.
|
|
563
|
+
- **`medium`**: the name is unique in GeoNames and the place is city-like, without independent confirmation.
|
|
564
|
+
- **`low`**: only a name match in GeoNames. Good for rough display; errors of several kilometres are possible.
|
|
565
|
+
|
|
566
|
+
Every coordinate is checked in CI against the province boundaries, and all 1,152 lie inside their own province. For the other 100 cities `coordinates` is `undefined`; no guessed values are filled in ([list and reasons](./reports/coordinates-not-imported.csv)).
|
|
567
|
+
|
|
568
|
+
Neighbors are computed from the boundaries (a shared border of at least 3 km) and reflect current borders, including the 2013 move of Tabas county from Yazd to South Khorasan.
|
|
569
|
+
|
|
570
|
+
Ids never change and are never reused. Renamed slugs keep working, and every change is listed in `DATA_CHANGES`.
|
|
571
|
+
|
|
572
|
+
## Compatibility
|
|
573
|
+
|
|
574
|
+
Every release is tested in CI against this matrix:
|
|
575
|
+
|
|
576
|
+
| | Supported |
|
|
577
|
+
|---|---|
|
|
578
|
+
| Node.js | 14, 16, 18, 20, 22, 24 (CommonJS and ES modules) |
|
|
579
|
+
| Deno, Bun | Current versions |
|
|
580
|
+
| TypeScript | 3.9 to 7, with `moduleResolution` `node`, `node16` or `bundler` |
|
|
581
|
+
| React | 16.8+ (tested with 17, 18 and 19) |
|
|
582
|
+
| Next.js | 14, 15, 16 (App Router; Turbopack and webpack) |
|
|
583
|
+
| Zod, Valibot | Zod 3.25+ and 4, Valibot 1 |
|
|
584
|
+
|
|
585
|
+
Any bundler that understands the `exports` field (Vite, webpack, Rollup, Rolldown, esbuild, Turbopack and others) can use the package.
|
|
586
|
+
|
|
587
|
+
## Contributing
|
|
588
|
+
|
|
589
|
+
Data corrections are the most valuable contribution; please include a source. See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
282
590
|
|
|
283
591
|
```bash
|
|
284
592
|
npm install
|
|
285
|
-
npm
|
|
286
|
-
npm run
|
|
287
|
-
npm run
|
|
288
|
-
npm test # validate + typecheck
|
|
593
|
+
npm test # data validation, TypeScript 7, Oxlint, Vitest
|
|
594
|
+
npm run build # tsdown (Rolldown + Oxc)
|
|
595
|
+
npm run test:dist # smoke test of the built files
|
|
289
596
|
```
|
|
290
597
|
|
|
291
|
-
|
|
598
|
+
To publish a new version, update `version` in `package.json` and `CHANGELOG.md`, then run:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
npm publish --access public
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
`npm publish` runs every check first (tests, build, smoke test, package lint and size budget) and stops if any of them fails.
|
|
605
|
+
|
|
606
|
+
## License and credits
|
|
607
|
+
|
|
608
|
+
| | Source | License |
|
|
609
|
+
|---|---|---|
|
|
610
|
+
| Code | This project | [MIT](./LICENSE) |
|
|
611
|
+
| Coordinates | [GeoNames](https://www.geonames.org) | CC BY 4.0 |
|
|
612
|
+
| Boundaries and neighbors | [geoBoundaries](https://www.geoboundaries.org) / © OpenStreetMap contributors | ODbL 1.0 |
|
|
292
613
|
|
|
293
|
-
|
|
614
|
+
The package's license field is `MIT AND CC-BY-4.0 AND ODbL-1.0`. The ODbL only applies if you redistribute the boundary data itself; see [`NOTICE.md`](./NOTICE.md).
|
|
294
615
|
|
|
295
|
-
|
|
616
|
+
<div align="center">
|
|
617
|
+
<sub>Made with ❤️ for Persian developers · <a href="./CHANGELOG.md">Changelog</a> · <a href="./MIGRATION.md">Migration guide</a> · <a href="https://mjavadsf.github.io/Iran-Cities/">Docs</a> · <a href="https://github.com/MJavadSF/Iran-Cities/issues">Issues</a></sub>
|
|
618
|
+
</div>
|