@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.
Files changed (130) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION.fa.md +277 -0
  4. package/MIGRATION.md +272 -0
  5. package/NOTICE.md +38 -0
  6. package/README.fa.md +528 -201
  7. package/README.md +524 -201
  8. package/dist/boundaries.cjs +9218 -0
  9. package/dist/boundaries.cjs.map +1 -0
  10. package/dist/boundaries.d.cts +30 -0
  11. package/dist/boundaries.d.mts +30 -0
  12. package/dist/boundaries.mjs +9215 -0
  13. package/dist/boundaries.mjs.map +1 -0
  14. package/dist/changes-B-wz9-Gy.cjs +841 -0
  15. package/dist/changes-B-wz9-Gy.cjs.map +1 -0
  16. package/dist/changes-CMVZbfZO.d.mts +8 -0
  17. package/dist/changes-DLZkHBwZ.mjs +800 -0
  18. package/dist/changes-DLZkHBwZ.mjs.map +1 -0
  19. package/dist/changes-DnnwRAKl.d.cts +8 -0
  20. package/dist/components-B7060em0.mjs +240 -0
  21. package/dist/components-B7060em0.mjs.map +1 -0
  22. package/dist/components-BuMUB7XT.d.cts +47 -0
  23. package/dist/components-DZz72pd1.d.mts +47 -0
  24. package/dist/components-grWmOsaV.cjs +247 -0
  25. package/dist/components-grWmOsaV.cjs.map +1 -0
  26. package/dist/components.css +251 -0
  27. package/dist/data/cities.csv +1253 -0
  28. package/dist/data/cities.json +23305 -0
  29. package/dist/data/iran-cities.sql +1487 -0
  30. package/dist/data/iran-provinces.geojson +1 -0
  31. package/dist/data/provinces.csv +32 -0
  32. package/dist/data/provinces.json +460 -0
  33. package/dist/hooks-C6kjWLIE.mjs +134 -0
  34. package/dist/hooks-C6kjWLIE.mjs.map +1 -0
  35. package/dist/hooks-K7pTMF2R.cjs +139 -0
  36. package/dist/hooks-K7pTMF2R.cjs.map +1 -0
  37. package/dist/index.cjs +30 -7550
  38. package/dist/index.d.cts +25 -0
  39. package/dist/index.d.mts +25 -25
  40. package/dist/index.mjs +3 -7507
  41. package/dist/lite-CtNPRhU-.mjs +1926 -0
  42. package/dist/lite-CtNPRhU-.mjs.map +1 -0
  43. package/dist/lite-CutarzAc.cjs +2051 -0
  44. package/dist/lite-CutarzAc.cjs.map +1 -0
  45. package/dist/lite-react-1HVWJOWt.cjs +55 -0
  46. package/dist/lite-react-1HVWJOWt.cjs.map +1 -0
  47. package/dist/lite-react-Ds-DNmEi.mjs +20 -0
  48. package/dist/lite-react-Ds-DNmEi.mjs.map +1 -0
  49. package/dist/lite-react-components.cjs +12 -0
  50. package/dist/lite-react-components.cjs.map +1 -0
  51. package/dist/lite-react-components.d.cts +5 -0
  52. package/dist/lite-react-components.d.mts +5 -0
  53. package/dist/lite-react-components.mjs +11 -0
  54. package/dist/lite-react-components.mjs.map +1 -0
  55. package/dist/lite-react.cjs +8 -0
  56. package/dist/lite-react.d.cts +7 -0
  57. package/dist/lite-react.d.mts +7 -0
  58. package/dist/lite-react.mjs +2 -0
  59. package/dist/lite.cjs +28 -0
  60. package/dist/lite.d.cts +23 -0
  61. package/dist/lite.d.mts +23 -0
  62. package/dist/lite.mjs +3 -0
  63. package/dist/react-BKmJ476x.mjs +20 -0
  64. package/dist/react-BKmJ476x.mjs.map +1 -0
  65. package/dist/react-BZV3TkXB.cjs +55 -0
  66. package/dist/react-BZV3TkXB.cjs.map +1 -0
  67. package/dist/react-components.cjs +12 -0
  68. package/dist/react-components.cjs.map +1 -0
  69. package/dist/react-components.d.cts +5 -0
  70. package/dist/react-components.d.mts +5 -0
  71. package/dist/react-components.mjs +11 -0
  72. package/dist/react-components.mjs.map +1 -0
  73. package/dist/react.cjs +8 -7553
  74. package/dist/react.d.cts +7 -0
  75. package/dist/react.d.mts +7 -30
  76. package/dist/react.mjs +2 -7524
  77. package/dist/rolldown-runtime-78lK1XfQ.cjs +42 -0
  78. package/dist/rolldown-runtime-D3KUYPsf.mjs +12 -0
  79. package/dist/src-DRaUVbc4.mjs +4302 -0
  80. package/dist/src-DRaUVbc4.mjs.map +1 -0
  81. package/dist/src-mqZTwJfQ.cjs +4439 -0
  82. package/dist/src-mqZTwJfQ.cjs.map +1 -0
  83. package/dist/types/boundaries.d.ts +30 -0
  84. package/dist/types/changes.d.ts +4 -0
  85. package/dist/types/components.d.ts +47 -0
  86. package/dist/types/core.d.ts +25 -0
  87. package/dist/types/geo.d.ts +2 -0
  88. package/dist/types/hooks.d.ts +2 -0
  89. package/dist/types/index.d.ts +28 -0
  90. package/dist/types/keyboard.d.ts +3 -0
  91. package/dist/types/lite-react-components.d.ts +5 -0
  92. package/dist/types/lite-react.d.ts +7 -0
  93. package/dist/types/lite.d.ts +26 -0
  94. package/dist/types/normalize.d.ts +1 -0
  95. package/dist/types/packed.d.ts +16 -0
  96. package/dist/types/react-components.d.ts +5 -0
  97. package/dist/types/react.d.ts +7 -0
  98. package/dist/types/types.d.ts +149 -0
  99. package/dist/types/valibot.d.ts +23 -0
  100. package/dist/types/validation-shared.d.ts +11 -0
  101. package/dist/types/zod.d.ts +23 -0
  102. package/dist/types-Ds_b0yHP.d.cts +150 -0
  103. package/dist/types-Ds_b0yHP.d.mts +150 -0
  104. package/dist/valibot.cjs +47 -0
  105. package/dist/valibot.cjs.map +1 -0
  106. package/dist/valibot.d.cts +21 -0
  107. package/dist/valibot.d.mts +21 -0
  108. package/dist/valibot.mjs +38 -0
  109. package/dist/valibot.mjs.map +1 -0
  110. package/dist/validation-shared-BFilTuY1.cjs +58 -0
  111. package/dist/validation-shared-BFilTuY1.cjs.map +1 -0
  112. package/dist/validation-shared-C7l2cv4e.d.cts +8 -0
  113. package/dist/validation-shared-CJjGRx8p.d.mts +7 -0
  114. package/dist/validation-shared-mh_saZne.mjs +29 -0
  115. package/dist/validation-shared-mh_saZne.mjs.map +1 -0
  116. package/dist/zod.cjs +49 -0
  117. package/dist/zod.cjs.map +1 -0
  118. package/dist/zod.d.cts +21 -0
  119. package/dist/zod.d.mts +21 -0
  120. package/dist/zod.mjs +42 -0
  121. package/dist/zod.mjs.map +1 -0
  122. package/package.json +177 -29
  123. package/dist/index.cjs.map +0 -1
  124. package/dist/index.d.ts +0 -25
  125. package/dist/index.mjs.map +0 -1
  126. package/dist/react.cjs.map +0 -1
  127. package/dist/react.d.ts +0 -30
  128. package/dist/react.mjs.map +0 -1
  129. package/dist/types-DzSw8Uw2.d.mts +0 -52
  130. package/dist/types-DzSw8Uw2.d.ts +0 -52
package/README.md CHANGED
@@ -1,295 +1,618 @@
1
- English | [فارسی](./README.fa.md)
1
+ <div align="center">
2
2
 
3
- # @code-plate/iran-cities
3
+ English · [فارسی](./README.fa.md)
4
4
 
5
- A complete, ready-to-use list of **Iran's provinces and cities** (Persian + English), packaged as a lightweight library with no forced dependencies, usable in **any JavaScript/TypeScript project**:
5
+ # 🇮🇷 Iran Cities
6
6
 
7
- - ✅ React / Next.js (App Router and Pages Router)
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
- The core of the package has **zero dependencies** and ships as both **CJS** and **ESM**. There's also an optional subpath (`@code-plate/iran-cities/react`) for React users with a few ready-made hooks; if your project isn't React, that file is never imported and React doesn't need to be installed at all.
9
+ Zero dependencies · Tree-shakeable · Fully typed · Built for Next.js
10
+
11
+ [![npm version](https://img.shields.io/npm/v/@code-plate/iran-cities?color=0a7f5a&label=npm)](https://www.npmjs.com/package/@code-plate/iran-cities)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@code-plate/iran-cities?color=0a7f5a)](https://www.npmjs.com/package/@code-plate/iran-cities)
13
+ [![CI](https://github.com/MJavadSF/Iran-Cities/actions/workflows/ci.yml/badge.svg)](https://github.com/MJavadSF/Iran-Cities/actions/workflows/ci.yml)
14
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/MJavadSF/Iran-Cities/badge)](https://scorecard.dev/viewer/?uri=github.com/MJavadSF/Iran-Cities)
15
+ [![types](https://img.shields.io/badge/types-included-3178c6)](#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
- # or
21
- yarn add @code-plate/iran-cities
22
- # or
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
- ## Data
90
+ </details>
91
+
92
+ ## Entry points
27
93
 
28
- - `31` provinces
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
- ## Usage in plain JavaScript / TypeScript (and Node.js/Express)
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
- ### A note on search
108
- `getProvince`, `getCities`, `getCity` and `getProvinceOfCity` compare strings in a **normalized** way — differences between Persian «ی» and Arabic «ي», «ک» vs. «ك», zero-width non-joiners, spaces and hyphens are all ignored. So `getCity('كرج')` and `getCity('کرج')` both work.
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
- ## Usage in Node.js / Express (backend)
129
+ ### Search and autocomplete
113
130
 
114
- ```js
115
- const express = require('express');
116
- const { getProvincesList, getCities, searchCities } = require('@code-plate/iran-cities');
131
+ ```ts
132
+ import { searchCities, searchProvinces } from '@code-plate/iran-cities';
117
133
 
118
- const app = express();
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
- app.get('/api/provinces', (req, res) => {
121
- res.json(getProvincesList());
122
- });
141
+ ### Neighbors, capitals and area codes
123
142
 
124
- app.get('/api/cities/:province', (req, res) => {
125
- res.json(getCities(req.params.province));
126
- });
143
+ ```ts
144
+ import { getNeighborProvinces, areNeighborProvinces, getCapital, getProvinceByPhoneCode } from '@code-plate/iran-cities';
127
145
 
128
- app.get('/api/cities/search', (req, res) => {
129
- res.json(searchCities(req.query.q ?? '', { limit: 20 }));
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
- ## Usage in React / Next.js
159
+ ## Next.js
136
160
 
137
- An optional subpath is available for React with a few ready-made hooks:
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
- import { useProvinces, useCities, useProvinceCitySelect, useCitySearch } from '@code-plate/iran-cities/react';
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
- function AddressForm() {
143
- const { provinces, cities, province, setProvince, city, setCity, provinceId, cityId, selectedCity } =
144
- useProvinceCitySelect();
175
+ ```tsx
176
+ // app/address/address-form.tsx (Client Component)
177
+ 'use client';
145
178
 
146
- // On submit, send { provinceId, cityId } to your backend.
147
- // selectedCity => { id, en, fa, slug, provinceId, provinceEn, provinceFa }
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
- <select value={province} onChange={(e) => setProvince(e.target.value)}>
152
- <option value="">Select a province</option>
153
- {provinces.map((p) => (
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
- Live city search (autocomplete):
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
- import { useCitySearch } from '@code-plate/iran-cities/react';
214
+ 'use client';
173
215
 
174
- function CityAutocomplete() {
175
- const { query, setQuery, results } = useCitySearch('', { limit: 8 });
216
+ import '@code-plate/iran-cities/components.css';
217
+ import { CityCombobox, ProvinceCitySelect } from '@code-plate/iran-cities/lite/react/components';
176
218
 
177
- return (
178
- <div>
179
- <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="City name..." />
180
- <ul>
181
- {results.map((c) => (
182
- <li key={c.id}>{c.fa} ({c.provinceFa})</li>
183
- ))}
184
- </ul>
185
- </div>
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
- Hooks available in the `react` subpath:
239
+ <details>
240
+ <summary>All CSS variables</summary>
191
241
 
192
- | Hook | Purpose |
242
+ | Variable | |
193
243
  |---|---|
194
- | `useProvinces()` | List of provinces |
195
- | `useCities(provinceQuery)` | Cities of a province, reacts to changes in the parameter |
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()` |
197
- | `useCitySearch(initialQuery?, options?)` | Live city search state |
198
- | `useProvinceSearch(initialQuery?, options?)` | Live province search state |
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
- > If your project is Vue, Angular, or anything else, just use the main entry point (`@code-plate/iran-cities`) and plug the raw functions (`getProvinces`, `getCities`, ...) into your own computed/signal/observable — no extra tooling required.
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
- ## Usage in Vue 3 (example)
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 selectedProvince = ref('');
213
- const cities = computed(() => getCities(selectedProvince.value));
314
+ const province = ref('');
315
+ const cities = computed(() => getCities(province.value));
214
316
  </script>
215
317
 
216
318
  <template>
217
- <select v-model="selectedProvince">
218
- <option v-for="p in provinces" :key="p.en" :value="p.en">{{ p.fa }}</option>
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="!selectedProvince">
221
- <option v-for="c in cities" :key="c.en" :value="c.en">{{ c.fa }}</option>
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
- ## Usage in Angular (example service)
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
- getProvinces() {
361
+ provinces(): ProvinceListItem[] {
235
362
  return getProvincesList();
236
363
  }
237
- getCitiesOf(provinceEn: string): City[] {
238
- return getCities(provinceEn);
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
- ## Full API
407
+ ## API reference
246
408
 
247
- ### Core functions (import from `'@code-plate/iran-cities'`)
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
- | Function | Input | Output | Description |
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
- `SearchOptions`: `{ limit?: number; locale?: 'en' | 'fa' | 'both' }`
478
+ </details>
269
479
 
270
- ### Types
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 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; }
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
- ## Development
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 run build # outputs dist/ (cjs + esm + d.ts)
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
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
- > **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`.
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
- ## License
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
- MIT
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>