@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.fa.md CHANGED
@@ -1,295 +1,622 @@
1
- [English](./README.md) | فارسی
1
+ <div align="center" dir="rtl">
2
2
 
3
- # @code-plate/iran-cities
3
+ [English](./README.md) · فارسی
4
4
 
5
- لیست کامل و آماده‌ی **استان‌ها و شهرهای ایران** (فارسی + انگلیسی)، بسته‌بندی‌شده به شکل یک کتابخانه‌ی سبک، بدون هیچ وابستگی اجباری، که در **هر پروژه‌ی جاوااسکریپتی/تایپ‌اسکریپتی** قابل استفاده است:
5
+ # 🇮🇷 شهرهای ایران
6
6
 
7
- - ✅ React / Next.js (App Router و Pages Router)
8
- - ✅ Vue 2 / Vue 3
9
- - ✅ Angular
10
- - ✅ Svelte
11
- - ✅ Node.js / Express (بک‌اند خالص)
12
- - ✅ Vanilla JS (مرورگر یا هر باندلر دیگر)
7
+ **همه‌ی استان‌ها و شهرهای ایران، به فارسی و انگلیسی، با جستجوی سریع و هوشمند، مختصات، مرز استان‌ها و کامپوننت‌های آماده.**
13
8
 
14
- هسته‌ی اصلی پکیج **هیچ وابستگی‌ای** ندارد (zero-dependency) و هم به‌صورت **CJS** و هم **ESM** ارائه می‌شود. یک ساب‌پس اختیاری (`@code-plate/iran-cities/react`) هم برای کسانی که در React کار می‌کنند وجود دارد که چند هوک آماده می‌دهد؛ اگر پروژه‌تان React نیست، هرگز این فایل import نمی‌شود و React اصلاً لازم نیست نصب شود.
9
+ بدون وابستگی · tree-shakeable · تایپ کامل · ساخته‌شده برای 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
+ **[مستندات](https://mjavadsf.github.io/Iran-Cities/fa/)** · **[زمین بازی زنده](https://mjavadsf.github.io/Iran-Cities/fa/playground/)** · **[نقشه](https://mjavadsf.github.io/Iran-Cities/fa/guides/geography/)**
18
+
19
+ </div>
20
+
21
+ <div dir="rtl">
22
+
23
+ ```ts
24
+ import { getCity, searchCities, getNeighborProvinces } from '@code-plate/iran-cities';
25
+
26
+ getCity('شيراز'); // { id: 292, fa: 'شیراز', nameEn: 'Shiraz', provinceFa: 'فارس', phoneCode: '071', ... }
27
+ searchCities('jivhk'); // [تهران] با کیبورد انگلیسی تایپ شده
28
+ getNeighborProvinces('قم'); // [اصفهان، مرکزی، سمنان، تهران]
29
+ ```
30
+
31
+ <table dir="rtl">
32
+ <tr>
33
+ <td width="33%" valign="top">
34
+
35
+ ### 📍 کامل
36
+ **۳۱** استان و **۱۲۵۲** شهر با id ثابت، نام فارسی و انگلیسی، کد تلفن، مرکز استان، استان‌های همسایه و مختصات **۱۱۵۲** شهر که همگی با مرز استان‌ها بررسی شده‌اند.
37
+
38
+ </td>
39
+ <td width="33%" valign="top">
40
+
41
+ ### 🔎 فارسی‌فهم
42
+ «ي/ی»، «ك/ک»، «آ/ا»، نیم‌فاصله، اعراب و ارقام فارسی را می‌شناسد، غلط تایپی را تحمل می‌کند و متنی را که با کیبورد اشتباه تایپ شده خودش درست می‌کند.
43
+
44
+ </td>
45
+ <td width="33%" valign="top">
46
+
47
+ ### ⚡ آماده‌ی production
48
+ کامپوننت‌های دسترس‌پذیر با CSS خام، اسکیمای Zod و Valibot، هوک‌های `"use client"`، نسخه‌ی `lite`، داده‌ی freeze شده و تایپ‌هایی از TypeScript 3.9 تا 7.
49
+
50
+ </td>
51
+ </tr>
52
+ </table>
53
+
54
+ > [!TIP]
55
+ > از نسخه‌ی 1.x ارتقا می‌دهید؟ بیشتر پروژه‌ها به هیچ تغییری نیاز ندارند. [راهنمای مهاجرت](./MIGRATION.fa.md) را ببینید.
56
+
57
+ ## فهرست
58
+
59
+ - [نصب](#نصب)
60
+ - [ورودی‌ها](#ورودیها)
61
+ - [شروع سریع](#شروع-سریع)
62
+ - [Next.js](#nextjs)
63
+ - [کامپوننت‌ها](#کامپوننتها)
64
+ - [اعتبارسنجی فرم](#اعتبارسنجی-فرم)
65
+ - [هوک‌های React](#هوکهای-react)
66
+ - [نقشه و جغرافیا](#نقشه-و-جغرافیا)
67
+ - [Vue، Svelte، Angular و Express](#vue-svelte-angular-و-express)
68
+ - [زبان‌های دیگر: CSV، SQL، JSON](#زبانهای-دیگر-csv-sql-json)
69
+ - [مرجع API](#مرجع-api)
70
+ - [تطبیق متن چطور کار می‌کند](#تطبیق-متن-چطور-کار-میکند)
71
+ - [داده‌ها و منابع](#دادهها-و-منابع)
72
+ - [سازگاری](#سازگاری)
73
+ - [مشارکت](#مشارکت)
74
+ - [مجوز و منابع](#مجوز-و-منابع)
15
75
 
16
76
  ## نصب
17
77
 
18
78
  ```bash
19
79
  npm install @code-plate/iran-cities
20
- # یا
21
- yarn add @code-plate/iran-cities
22
- # یا
80
+ ```
81
+
82
+ <details>
83
+ <summary>pnpm، yarn، bun، Deno</summary>
84
+
85
+ ```bash
23
86
  pnpm add @code-plate/iran-cities
87
+ yarn add @code-plate/iran-cities
88
+ bun add @code-plate/iran-cities
89
+ deno add npm:@code-plate/iran-cities
24
90
  ```
25
91
 
26
- ## داده‌ها
92
+ </details>
27
93
 
28
- - `۳۱` استان
29
- - `۱۲۵۲` شهر
30
- - هر رکورد شامل `id` (شناسه‌ی عددی یکتا و پایدار)، `en` (شناسه لاتین kebab-case، مثل `tehran`) و `fa` (نام فارسی، مثل `تهران`) است.
31
- - هر استان `phoneCode` (کد تلفن بین‌شهری) و `capitalId` (id مرکز استان) دارد؛ هر شهر `isCapital` دارد و مراکز استان‌ها `coordinates: { lat, lng }` هم دارند.
32
- - `id` شهرها در **کل کشور یکتاست** (شهرهای هم‌نام مثل «مهاباد» در دو استان، دو id متفاوت دارند) و بین نسخه‌ها تغییر نمی‌کند؛ شهر جدید همیشه id بعدی را می‌گیرد.
94
+ ## ورودی‌ها
33
95
 
34
- ---
96
+ کوچک‌ترین ورودی‌ای را انتخاب کنید که نیازتان را پوشش می‌دهد. حجم‌ها بعد از minify و فشرده‌سازی brotli و همراه با داده‌ها هستند.
35
97
 
36
- ## استفاده در جاوااسکریپت / تایپ‌اسکریپت خالص (و Node.js/Express)
98
+ | import | محتوا | حجم |
99
+ |---|---|---:|
100
+ | `@code-plate/iran-cities` | همه‌ی توابع، از جمله مختصات و فاصله | ۲۴ کیلوبایت |
101
+ | `@code-plate/iran-cities/lite` | همان جستجو و lookup، بدون مختصات | ۱۶ کیلوبایت |
102
+ | `@code-plate/iran-cities/react` | هوک‌های React | ۲۵ کیلوبایت |
103
+ | `@code-plate/iran-cities/lite/react` | هوک‌های React بدون مختصات | ۱۷ کیلوبایت |
104
+ | `@code-plate/iran-cities/lite/react/components` | `CityCombobox` و `ProvinceCitySelect` | ۱۸ کیلوبایت |
105
+ | `@code-plate/iran-cities/react/components` | همان کامپوننت‌ها همراه با مختصات | ۲۶ کیلوبایت |
106
+ | `@code-plate/iran-cities/components.css` | استایل کامپوننت‌ها | ۱٫۵ کیلوبایت |
107
+ | `@code-plate/iran-cities/zod` · `/valibot` | اسکیمای اعتبارسنجی | ۱۶ کیلوبایت |
108
+ | `@code-plate/iran-cities/boundaries` | مرز استان‌ها (GeoJSON) و `getProvinceAt` | ۵۳ کیلوبایت |
109
+ | `@code-plate/iran-cities/data/*` | فایل‌های CSV، SQL، JSON و GeoJSON | |
110
+
111
+ ورودی‌ها کد مشترک دارند، پس import کردن چند ورودی هیچ‌وقت داده را دو بار ship نمی‌کند. React، Zod و Valibot وابستگی peer اختیاری هستند.
112
+
113
+ ## شروع سریع
114
+
115
+ ### پیدا کردن
37
116
 
38
117
  ```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
- // همه‌ی استان‌ها به همراه شهرهایشان
56
- getProvinces();
57
- // [{ id: 1, en: 'alborz', fa: 'البرز', cities: [{ id: 1, en: 'karaj', fa: 'کرج', slug: 'alborz/karaj' }, ...] }, ...]
58
-
59
- // فقط لیست استان‌ها (بدون شهر) - برای Select/Dropdown
60
- getProvincesList();
61
- // [{ id: 1, en: 'alborz', fa: 'البرز' }, { id: 2, en: 'ardabil', fa: 'اردبیل' }, ...]
62
-
63
- // گرفتن یک استان با نام انگلیسی یا فارسی
64
- getProvince('tehran');
65
- getProvince('تهران'); // همین نتیجه را می‌دهد
66
- getProvince(28); // با id عددی هم کار می‌کند (رشته‌ی عددی '28' هم قبول است)
67
-
68
- // شهرهای یک استان خاص
69
- getCities('fars'); // بر اساس شناسه انگلیسی
70
- getCities('فارس'); // یا بر اساس نام فارسی، هر دو یکسان کار می‌کنند
71
-
72
- // پیدا کردن یک شهر مشخص (در کل کشور یا محدود به یک استان)
73
- getCity('shiraz');
74
- getCity('شیراز', 'fars'); // محدود به استان فارس
75
-
76
- // لیست مسطح (flat) همه‌ی شهرهای کشور همراه با استان مربوطه
77
- getAllCities();
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)]
86
-
87
- // پیدا کردن استانِ یک شهر
88
- getProvinceOfCity('اصفهان'); // => { id: 6, en: 'isfahan', fa: 'اصفهان', cities: [...] }
89
- getProvinceOfCity(292); // با id شهر هم کار می‌کند
90
-
91
- // جستجوی زنده (autocomplete) در استان‌ها یا شهرها
92
- searchProvinces('خراسان'); // هر سه استان خراسان را برمی‌گرداند
93
- searchCities('شهر', { limit: 10 }); // ۱۰ نتیجه‌ی اول شامل «شهر»
94
- searchCities('mashhad', { locale: 'en' }); // جستجو فقط در نام انگلیسی
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
-
103
- // آمار کلی
104
- getStats(); // { provinceCount: 31, cityCount: 1252 }
105
- ```
118
+ import { getProvince, getCities, getCity, getCityById, getProvinceOfCity } from '@code-plate/iran-cities';
106
119
 
107
- ### نکته‌ی مهم درباره‌ی جستجو
108
- تابع‌های `getProvince`, `getCities`, `getCity` و `getProvinceOfCity` به‌صورت **نرمال‌شده** مقایسه می‌کنند؛ یعنی تفاوت «ی» فارسی/«ي» عربی، «ک»/«ك»، نیم‌فاصله، فاصله و خط‌تیره را نادیده می‌گیرند. پس `getCity('كرج')` و `getCity('کرج')` هر دو کار می‌کنند.
120
+ getProvince('tehran'); // با شناسه‌ی انگلیسی
121
+ getProvince('تهران'); // با نام فارسی
122
+ getProvince(28); // با id (رشته‌ی '28' و '۲۸' هم کار می‌کند)
109
123
 
110
- ---
124
+ getCities('fars'); // همه‌ی شهرهای فارس
125
+ getCity('شیراز'); // یک شهر در کل ایران
126
+ getCity('شیراز', 'fars'); // یک شهر داخل یک استان
127
+ getCityById(292); // با id سراسری
128
+ getProvinceOfCity('اصفهان'); // استانی که شهر در آن است
129
+ ```
111
130
 
112
- ## استفاده در Node.js / Express (بک‌اند)
131
+ ### جستجو و autocomplete
113
132
 
114
- ```js
115
- const express = require('express');
116
- const { getProvincesList, getCities, searchCities } = require('@code-plate/iran-cities');
133
+ ```ts
134
+ import { searchCities, searchProvinces } from '@code-plate/iran-cities';
117
135
 
118
- const app = express();
136
+ searchProvinces('خراسان'); // [خراسان جنوبی، خراسان رضوی، خراسان شمالی]
137
+ searchCities('شهر', { limit: 10 }); // ۱۰ نتیجه‌ی اول
138
+ searchCities('تهرن', { fuzzy: true }); // تحمل غلط تایپی: [تهران]
139
+ searchCities('jivhk'); // کیبورد اشتباه: [تهران]
140
+ searchCities('', { emptyQuery: 'none' }); // [] به‌جای هر ۱۲۵۲ شهر
141
+ ```
119
142
 
120
- app.get('/api/provinces', (req, res) => {
121
- res.json(getProvincesList());
122
- });
143
+ ### همسایه‌ها، مرکز استان و کد تلفن
123
144
 
124
- app.get('/api/cities/:province', (req, res) => {
125
- res.json(getCities(req.params.province));
126
- });
145
+ ```ts
146
+ import { getNeighborProvinces, areNeighborProvinces, getCapital, getProvinceByPhoneCode } from '@code-plate/iran-cities';
127
147
 
128
- app.get('/api/cities/search', (req, res) => {
129
- res.json(searchCities(req.query.q ?? '', { limit: 20 }));
130
- });
148
+ getNeighborProvinces('قم'); // [اصفهان، مرکزی، سمنان، تهران]
149
+ areNeighborProvinces('yazd', 'south-khorasan'); // true
150
+ getCapital('khuzestan'); // اهواز
151
+ getProvinceByPhoneCode('021'); // تهران ('21'، 21، '۰۲۱' و '+98 21' هم قبول است)
131
152
  ```
132
153
 
133
- ---
154
+ ### نام نمایشی انگلیسی
155
+
156
+ ```ts
157
+ getProvince(16)?.nameEn; // 'Kohgiluyeh and Boyer-Ahmad' (شناسه‌ی en برابر 'kohgiluyeh-and-boyer-ahmad' است)
158
+ getCity('bandar-e-gaz')?.nameEn; // 'Bandar-e Gaz'
159
+ ```
134
160
 
135
- ## استفاده در React / Next.js
161
+ ## Next.js
136
162
 
137
- برای React یک ساب‌پس اختیاری وجود دارد که چند هوک آماده ارائه می‌دهد:
163
+ در **Server Componentها** از ورودی اصلی و در **Client Componentها** از هوک‌ها یا کامپوننت‌ها استفاده کنید. همه‌ی ورودی‌های React خودشان `"use client"` دارند.
138
164
 
139
165
  ```tsx
140
- import { useProvinces, useCities, useProvinceCitySelect, useCitySearch } from '@code-plate/iran-cities/react';
166
+ // app/address/page.tsx (Server Component)
167
+ import { getCity } from '@code-plate/iran-cities';
168
+ import AddressForm from './address-form';
169
+ import { save } from './actions';
170
+
171
+ export default function Page() {
172
+ const city = getCity('شیراز', 'fars');
173
+ return <AddressForm action={save} defaultCityId={city?.id} />;
174
+ }
175
+ ```
141
176
 
142
- function AddressForm() {
143
- const { provinces, cities, province, setProvince, city, setCity, provinceId, cityId, selectedCity } =
144
- useProvinceCitySelect();
177
+ ```tsx
178
+ // app/address/address-form.tsx (Client Component)
179
+ 'use client';
145
180
 
146
- // موقع ارسال فرم: { provinceId, cityId } را به بک‌اند بفرستید
147
- // selectedCity => { id, en, fa, slug, provinceId, provinceEn, provinceFa }
181
+ import '@code-plate/iran-cities/components.css';
182
+ import { ProvinceCitySelect } from '@code-plate/iran-cities/lite/react/components';
148
183
 
184
+ export default function AddressForm({ action, defaultCityId }: { action: (data: FormData) => void; defaultCityId?: number }) {
149
185
  return (
150
- <>
151
- <select value={province} onChange={(e) => setProvince(e.target.value)}>
152
- <option value="">انتخاب استان</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="">انتخاب شهر</option>
160
- {cities.map((c) => (
161
- <option key={c.id} value={c.en}>{c.fa}</option>
162
- ))}
163
- </select>
164
- </>
186
+ <form action={action}>
187
+ <ProvinceCitySelect provinceName="provinceId" cityName="cityId" defaultValue={{ cityId: defaultCityId }} required />
188
+ <button type="submit">ثبت</button>
189
+ </form>
165
190
  );
166
191
  }
167
192
  ```
168
193
 
169
- جستجوی زنده‌ی شهر (autocomplete):
194
+ ```ts
195
+ // app/address/actions.ts
196
+ 'use server';
197
+
198
+ import { provinceCitySchema } from '@code-plate/iran-cities/zod';
199
+
200
+ export async function save(formData: FormData) {
201
+ const result = provinceCitySchema.safeParse({ provinceId: formData.get('provinceId'), cityId: formData.get('cityId') });
202
+ if (!result.success) return;
203
+ }
204
+ ```
205
+
206
+ > [!TIP]
207
+ > در Client Componentها از ورودی‌های `lite` استفاده کنید، مگر اینکه مرورگر واقعاً مختصات لازم داشته باشد. این کار حدود ۸ کیلوبایت از bundle کلاینت کم می‌کند.
208
+
209
+ با Next.js 14، 15 و 16 و با Turbopack و webpack تست شده است.
210
+
211
+ ## کامپوننت‌ها
212
+
213
+ دو کامپوننت دسترس‌پذیر که با یک فایل کوچک CSS خام استایل شده‌اند (بدون Tailwind و بدون CSS-in-JS). [نسخه‌ی زنده را ببینید](https://mjavadsf.github.io/Iran-Cities/fa/guides/components/).
170
214
 
171
215
  ```tsx
172
- import { useCitySearch } from '@code-plate/iran-cities/react';
216
+ 'use client';
173
217
 
174
- function CityAutocomplete() {
175
- const { query, setQuery, results } = useCitySearch('', { limit: 8 });
218
+ import '@code-plate/iran-cities/components.css';
219
+ import { CityCombobox, ProvinceCitySelect } from '@code-plate/iran-cities/lite/react/components';
176
220
 
177
- return (
178
- <div>
179
- <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="نام شهر..." />
180
- <ul>
181
- {results.map((c) => (
182
- <li key={c.id}>{c.fa} ({c.provinceFa})</li>
183
- ))}
184
- </ul>
185
- </div>
186
- );
221
+ <ProvinceCitySelect provinceName="provinceId" cityName="cityId" onChange={({ provinceId, cityId }) => {}} />
222
+ <CityCombobox label="شهر مقصد" name="destinationId" province="fars" onChange={(city) => {}} />
223
+ ```
224
+
225
+ - **`CityCombobox`** از [الگوی combobox در WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/) پیروی می‌کند: `↑` و `↓`، `Home` و `End`، `Enter` برای انتخاب، `Escape` برای بستن و `Escape` دوم برای پاک کردن. غلط تایپی را تحمل می‌کند و کیبورد اشتباه را درست می‌کند.
226
+ - **`ProvinceCitySelect`** دو `<select>` بومی با label است و با تغییر استان، شهر خالی می‌شود.
227
+ - هر دو **id** را از طریق فیلدهای معمولی فرم (`name`) ارسال می‌کنند، پس با فرم معمولی و Server Action کار می‌کنند. هر دو هم controlled (`value` و `onChange`) و هم uncontrolled (`defaultValue`) کار می‌کنند و بدون تغییر داخل `Controller` در react-hook-form قرار می‌گیرند.
228
+ - پیش‌فرض راست‌به‌چپ (`dir="rtl"`)، نام انگلیسی با `locale="en"`، و حالت تاریک خودکار.
229
+
230
+ تم را با متغیرهای CSS روی هر والدی تغییر دهید. همه‌ی selectorها با `:where()` نوشته شده‌اند، پس کلاس‌های خودتان بدون `!important` اولویت دارند:
231
+
232
+ ```css
233
+ .checkout {
234
+ --ic-accent: #2563eb;
235
+ --ic-radius: 0.375rem;
236
+ --ic-height: 2.5rem;
237
+ --ic-font: 'Vazirmatn', sans-serif;
187
238
  }
188
239
  ```
189
240
 
190
- هوک‌های در دسترس در ساب‌پس `react`:
241
+ <details>
242
+ <summary>همه‌ی متغیرهای CSS</summary>
191
243
 
192
- | هوک | کاربرد |
244
+ | متغیر | |
193
245
  |---|---|
194
- | `useProvinces()` | لیست استان‌ها |
195
- | `useCities(provinceQuery)` | شهرهای یک استان، وابسته به تغییر پارامتر |
196
- | `useProvinceCitySelect()` | state کامل یک فرم استان→شهر (با ریست خودکار شهر هنگام تغییر استان)؛ علاوه بر `province`/`city` مقدارهای `provinceId`, `cityId`, `selectedProvince`, `selectedCity` و `reset()` را هم برمی‌گرداند |
197
- | `useCitySearch(initialQuery?, options?)` | state جستجوی زنده‌ی شهر |
198
- | `useProvinceSearch(initialQuery?, options?)` | state جستجوی زنده‌ی استان |
246
+ | `--ic-font`، `--ic-font-size`، `--ic-line-height` | تایپوگرافی |
247
+ | `--ic-height`، `--ic-radius`، `--ic-gap`، `--ic-padding-inline` | اندازه و فاصله |
248
+ | `--ic-bg`، `--ic-fg`، `--ic-muted`، `--ic-border`، `--ic-border-hover` | رنگ‌ها |
249
+ | `--ic-accent`، `--ic-accent-soft`، `--ic-ring` | رنگ اصلی و حلقه‌ی focus |
250
+ | `--ic-danger`، `--ic-danger-ring` | حالت نامعتبر |
251
+ | `--ic-popover-bg`، `--ic-shadow`، `--ic-listbox-max-height` | فهرست پیشنهادها |
252
+ | `--ic-disabled-bg`، `--ic-transition` | سایر |
253
+
254
+ حالت تاریک از `prefers-color-scheme` پیروی می‌کند؛ برای اجبار، `data-theme="dark"` یا `"light"` را روی یک والد بگذارید.
255
+
256
+ </details>
257
+
258
+ ## اعتبارسنجی فرم
259
+
260
+ ```ts
261
+ import { provinceCitySchema, cityIdSchema, citySchema } from '@code-plate/iran-cities/zod';
262
+
263
+ provinceCitySchema.parse({ provinceId: '7', cityId: '292' }); // { provinceId: 7, cityId: 292 }
264
+ provinceCitySchema.safeParse({ provinceId: 28, cityId: 292 }); // خطا روی cityId: «این شهر در استان انتخاب‌شده نیست.»
265
+ cityIdSchema.parse('۲۹۲'); // 292
266
+ citySchema.parse('شیراز'); // شیء کامل شهر
267
+ ```
268
+
269
+ همین اسکیماها برای Valibot در `@code-plate/iran-cities/valibot` هم هست. پیام‌ها به‌طور پیش‌فرض فارسی‌اند؛ با `createZodSchemas({ ... })` یا `createValibotSchemas({ ... })` پیام خودتان را بدهید.
270
+
271
+ ## هوک‌های React
272
+
273
+ از `@code-plate/iran-cities/react` یا بدون مختصات از `@code-plate/iran-cities/lite/react` import کنید.
274
+
275
+ | هوک | خروجی |
276
+ |---|---|
277
+ | `useProvinces()` | فهرست استان‌ها |
278
+ | `useCities(province)` | شهرهای یک استان |
279
+ | `useProvinceCitySelect(options?)` | همه‌ی چیزهایی که یک فیلد استان ← شهر لازم دارد؛ گزینه‌ها: `initialProvince`، `initialCity`، `value`، `onChange` |
280
+ | `useCitySearch(initialQuery?, options?)` | `{ query, setQuery, results }` |
281
+ | `useProvinceSearch(initialQuery?, options?)` | `{ query, setQuery, results }` |
282
+
283
+ ```tsx
284
+ const { provinces, cities, province, city, setProvince, setCity, provinceId, cityId } = useProvinceCitySelect({ initialCity: 292 });
285
+ const [value, setValue] = useState<ProvinceCityValue>({ provinceId: 7, cityId: 292 });
286
+ useProvinceCitySelect({ value, onChange: setValue }); // controlled
287
+ ```
288
+
289
+ ## نقشه و جغرافیا
290
+
291
+ ```ts
292
+ import { getDistance, getNearestCities } from '@code-plate/iran-cities';
293
+ import { getProvinceAt, provinceBoundaries } from '@code-plate/iran-cities/boundaries';
294
+
295
+ getDistance('tehran', 'karaj'); // 36.5 (کیلومتر)
296
+ getNearestCities({ lat: 29.6, lng: 52.5 }, { limit: 1 }); // [{ city: شیراز, distanceKm: 5.6 }]
297
+ getProvinceAt({ lat: 34.64, lng: 50.88 }); // قم
298
+ getProvinceAt({ lat: 48.85, lng: 2.35 }); // undefined (خارج از ایران)
299
+ ```
300
+
301
+ `provinceBoundaries` یک `FeatureCollection` استاندارد GeoJSON است که مستقیم با Leaflet، MapLibre و deck.gl کار می‌کند. هر شهری که مختصات دارد به استان خودش برمی‌گردد. [نقشه را امتحان کنید](https://mjavadsf.github.io/Iran-Cities/fa/guides/geography/).
199
302
 
200
- > اگر پروژه‌تان Vue، Angular یا هر چیز دیگری است، فقط از مسیر اصلی (`@code-plate/iran-cities`) استفاده کنید و توابع خام (`getProvinces`, `getCities`, ...) را داخل computed/signal/observable خودتان قرار دهید — نیازی به هیچ ابزار اضافه‌ای نیست.
303
+ ## Vue، Svelte، Angular و Express
201
304
 
202
- ---
305
+ توابع اصلی توابع ساده و همگام هستند و در هر فریم‌ورکی کار می‌کنند.
203
306
 
204
- ## استفاده در Vue 3 (نمونه)
307
+ <details>
308
+ <summary><b>Vue 3</b></summary>
205
309
 
206
310
  ```vue
207
311
  <script setup>
208
312
  import { ref, computed } from 'vue';
209
- import { getProvincesList, getCities } from '@code-plate/iran-cities';
313
+ import { getProvincesList, getCities } from '@code-plate/iran-cities/lite';
210
314
 
211
315
  const provinces = getProvincesList();
212
- const selectedProvince = ref('');
213
- const cities = computed(() => getCities(selectedProvince.value));
316
+ const province = ref('');
317
+ const cities = computed(() => getCities(province.value));
214
318
  </script>
215
319
 
216
320
  <template>
217
- <select v-model="selectedProvince">
218
- <option v-for="p in provinces" :key="p.en" :value="p.en">{{ p.fa }}</option>
321
+ <select v-model="province">
322
+ <option v-for="p in provinces" :key="p.id" :value="p.en">{{ p.fa }}</option>
219
323
  </select>
220
- <select :disabled="!selectedProvince">
221
- <option v-for="c in cities" :key="c.en" :value="c.en">{{ c.fa }}</option>
324
+ <select :disabled="!province">
325
+ <option v-for="c in cities" :key="c.id" :value="c.en">{{ c.fa }}</option>
222
326
  </select>
223
327
  </template>
224
328
  ```
225
329
 
226
- ## استفاده در Angular (نمونه سرویس)
330
+ </details>
331
+
332
+ <details>
333
+ <summary><b>Svelte</b></summary>
334
+
335
+ ```svelte
336
+ <script>
337
+ import { getProvincesList, getCities } from '@code-plate/iran-cities/lite';
338
+
339
+ const provinces = getProvincesList();
340
+ let province = '';
341
+ $: cities = getCities(province);
342
+ </script>
343
+
344
+ <select bind:value={province}>
345
+ {#each provinces as p (p.id)}<option value={p.en}>{p.fa}</option>{/each}
346
+ </select>
347
+ <select disabled={!province}>
348
+ {#each cities as c (c.id)}<option value={c.en}>{c.fa}</option>{/each}
349
+ </select>
350
+ ```
351
+
352
+ </details>
353
+
354
+ <details>
355
+ <summary><b>Angular</b></summary>
227
356
 
228
357
  ```ts
229
358
  import { Injectable } from '@angular/core';
230
- import { getProvincesList, getCities, type City } from '@code-plate/iran-cities';
359
+ import { getProvincesList, getCities, type City, type ProvinceListItem } from '@code-plate/iran-cities/lite';
231
360
 
232
361
  @Injectable({ providedIn: 'root' })
233
362
  export class IranLocationsService {
234
- getProvinces() {
363
+ provinces(): ProvinceListItem[] {
235
364
  return getProvincesList();
236
365
  }
237
- getCitiesOf(provinceEn: string): City[] {
238
- return getCities(provinceEn);
366
+
367
+ citiesOf(province: string): readonly City[] {
368
+ return getCities(province);
239
369
  }
240
370
  }
241
371
  ```
242
372
 
243
- ---
373
+ </details>
374
+
375
+ <details>
376
+ <summary><b>Node.js / Express</b></summary>
377
+
378
+ ```js
379
+ const express = require('express');
380
+ const { getProvincesList, getCities, searchCities } = require('@code-plate/iran-cities');
381
+
382
+ const app = express();
383
+
384
+ app.get('/api/provinces', (req, res) => res.json(getProvincesList()));
385
+ app.get('/api/cities/search', (req, res) => res.json(searchCities(String(req.query.q ?? ''), { limit: 20 })));
386
+ app.get('/api/cities/:province', (req, res) => res.json(getCities(req.params.province)));
387
+ ```
388
+
389
+ مسیر جستجو قبل از `/:province` تعریف شده؛ وگرنه Express کلمه‌ی `search` را نام استان حساب می‌کند.
390
+
391
+ </details>
392
+
393
+ ## زبان‌های دیگر: CSV، SQL، JSON
394
+
395
+ پکیج همین داده‌ها را به‌صورت فایل معمولی هم دارد، برای Laravel، Django، Go یا هر دیتابیسی:
396
+
397
+ | فایل | |
398
+ |---|---|
399
+ | `data/provinces.csv`، `data/cities.csv` | CSV با کدگذاری UTF-8 و سطر عنوان |
400
+ | `data/provinces.json`، `data/cities.json` | آرایه‌ی مسطح JSON |
401
+ | `data/iran-cities.sql` | جدول‌ها، کلیدهای خارجی و insertها برای PostgreSQL، MySQL، MariaDB و SQLite |
402
+ | `data/iran-provinces.geojson` | مرز استان‌ها |
403
+
404
+ ```bash
405
+ curl -O https://unpkg.com/@code-plate/iran-cities/dist/data/iran-cities.sql
406
+ psql mydb < iran-cities.sql
407
+ ```
408
+
409
+ ## مرجع API
410
+
411
+ هر جا query خواسته شده، می‌توانید نام فارسی، شناسه‌ی انگلیسی، نام نمایشی انگلیسی یا id عددی (عدد یا رشته، حتی با ارقام فارسی) بدهید.
412
+
413
+ <details open>
414
+ <summary><b>استان‌ها</b></summary>
415
+
416
+ | تابع | خروجی |
417
+ |---|---|
418
+ | `getProvinces()` | `readonly Province[]` همراه با شهرها |
419
+ | `getProvincesList()` | `ProvinceListItem[]` بدون شهرها (هر بار آرایه‌ی جدید) |
420
+ | `getProvince(query)` | `Province \| undefined` |
421
+ | `getProvinceById(id)` | `Province \| undefined` |
422
+ | `getProvinceByPhoneCode(code)` | `Province \| undefined` |
423
+ | `getProvinceOfCity(cityQuery)` | `Province \| undefined` |
424
+ | `getNeighborProvinces(query)` | `Province[]` هم‌مرز |
425
+ | `areNeighborProvinces(a, b)` | `boolean` |
426
+
427
+ </details>
428
+
429
+ <details open>
430
+ <summary><b>شهرها</b></summary>
431
+
432
+ | تابع | خروجی |
433
+ |---|---|
434
+ | `getCities(provinceQuery)` | `readonly City[]` |
435
+ | `getAllCities()` | `readonly FlatCity[]` |
436
+ | `getCity(cityQuery, provinceQuery?)` | `FlatCity \| undefined` |
437
+ | `getCityById(id)` | `FlatCity \| undefined` |
438
+ | `getCityBySlug(slug)` | `FlatCity \| undefined`، مثلاً `'fars/shiraz'`؛ slugهای قدیمی هم کار می‌کنند |
439
+ | `getCitiesByName(name)` | `readonly FlatCity[]`، همه‌ی شهرهای هم‌نام یا با همان نام جایگزین |
440
+ | `isAmbiguousCityName(name)` | `boolean` |
441
+ | `getCapital(provinceQuery)` | `FlatCity \| undefined` |
442
+ | `getCapitals()` | `FlatCity[]`، مراکز ۳۱ استان |
443
+
444
+ </details>
445
+
446
+ <details open>
447
+ <summary><b>جستجو</b></summary>
448
+
449
+ | تابع | خروجی |
450
+ |---|---|
451
+ | `searchCities(query, options?)` | `FlatCity[]` |
452
+ | `searchProvinces(query, options?)` | `Province[]` |
453
+ | `normalize(text)` | `string`، همان نرمال‌سازی‌ای که همه‌ی lookupها استفاده می‌کنند |
454
+ | `convertKeyboardLayout(text, 'fa' \| 'en')` | `string` |
455
+
456
+ | `SearchOptions` | پیش‌فرض | |
457
+ |---|---|---|
458
+ | `limit` | بدون محدودیت | حداکثر تعداد نتایج |
459
+ | `locale` | `'both'` | `'fa'`، `'en'` یا `'both'` |
460
+ | `fuzzy` | `false` | تحمل غلط تایپی |
461
+ | `maxDistance` | `2` | حداکثر فاصله‌ی ویرایشی در حالت `fuzzy`؛ حداکثر یک‌سوم طول query |
462
+ | `emptyQuery` | `'all'` | خروجی query خالی: `'all'` یا `'none'` |
463
+ | `keyboardFix` | `true` | اگر نتیجه‌ای نبود، با چیدمان کیبورد دیگر دوباره جستجو می‌کند |
464
+
465
+ </details>
466
+
467
+ <details open>
468
+ <summary><b>جغرافیا</b></summary>
469
+
470
+ | تابع | خروجی |
471
+ |---|---|
472
+ | `getDistance(from, to)` | کیلومتر، یا `undefined` اگر یکی مختصات نداشته باشد |
473
+ | `getNearestCities(point, options?)` | `{ city, distanceKm }[]` به ترتیب فاصله؛ گزینه‌ها `limit` (۵، و `0` یعنی همه)، `minConfidence` (`'low'`)، `maxDistanceKm` |
474
+ | `getProvinceAt(point, options?)` | `Province \| undefined`؛ نقطه‌هایی که تا `maxDistanceKm` (۵) بیرون از مرز باشند به نزدیک‌ترین استان نسبت داده می‌شوند *(ورودی boundaries)* |
475
+ | `getProvinceBoundary(id)` | یک feature از GeoJSON *(ورودی boundaries)* |
476
+ | `provinceBoundaries` | `FeatureCollection` از GeoJSON *(ورودی boundaries)* |
477
+
478
+ `from`، `to` و `point` می‌توانند query شهر یا `{ lat, lng }` باشند.
244
479
 
245
- ## API کامل
480
+ </details>
246
481
 
247
- ### توابع اصلی (import از `'@code-plate/iran-cities'`)
482
+ <details open>
483
+ <summary><b>نسخه‌ی داده</b></summary>
248
484
 
249
- | تابع | ورودی | خروجی | توضیح |
250
- |---|---|---|---|
251
- | `getProvinces()` | - | `Province[]` | همه‌ی استان‌ها به همراه شهرها |
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` | یک شهر مشخص |
262
- | `getAllCities()` | - | `FlatCity[]` | همه‌ی شهرها به‌صورت مسطح |
263
- | `getProvinceOfCity(cityQuery)` | `string \| number` | `Province \| undefined` | استانِ یک شهر |
264
- | `searchProvinces(query, options?)` | `string, SearchOptions?` | `Province[]` | جستجوی fuzzy استان |
265
- | `searchCities(query, options?)` | `string, SearchOptions?` | `FlatCity[]` | جستجوی fuzzy شهر |
266
- | `getStats()` | - | `{provinceCount, cityCount}` | آمار کلی |
485
+ | export | |
486
+ |---|---|
487
+ | `DATA_VERSION` | نسخه‌ی داده، مثلاً `'2.0.0'` |
488
+ | `DATA_CHANGES` | همه‌ی تغییر نام‌ها، اصلاح‌های املایی و حذف‌ها از نسخه‌ی 1.0 |
489
+ | `getDataChanges(sinceVersion?)` | تغییرات بعد از یک نسخه، برای به‌روز کردن مقادیری که ذخیره کرده‌اید |
267
490
 
268
- `SearchOptions`: `{ limit?: number; locale?: 'en' | 'fa' | 'both' }`
491
+ </details>
269
492
 
270
- ### تایپ‌ها
493
+ ### TypeScript
271
494
 
272
495
  ```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; }
496
+ interface Province {
497
+ id: number; // 1 – 31
498
+ en: string; // 'fars'
499
+ fa: string; // 'فارس'
500
+ nameEn: string; // 'Fars'
501
+ phoneCode: string; // '071'
502
+ capitalId: number;
503
+ neighborIds: readonly number[];
504
+ cities: readonly City[];
505
+ }
506
+
507
+ interface City {
508
+ id: number; // در کل ایران یکتا و هرگز دوباره استفاده نمی‌شود
509
+ en: string; // 'shiraz'
510
+ fa: string; // 'شیراز'
511
+ nameEn: string; // 'Shiraz'
512
+ slug: string; // 'fars/shiraz'
513
+ isCapital: boolean;
514
+ aliases: readonly string[];
515
+ coordinates?: { lat: number; lng: number; confidence: 'high' | 'medium' | 'low' };
516
+ }
517
+
518
+ interface FlatCity extends City {
519
+ provinceId: number;
520
+ provinceEn: string;
521
+ provinceFa: string;
522
+ provinceNameEn: string;
523
+ phoneCode: string;
524
+ }
277
525
  ```
278
526
 
279
- ---
527
+ همه‌ی تایپ‌ها export شده‌اند، از جمله `ProvinceListItem`، `SearchOptions`، `NearestCity`، `LatLng`، `ProvinceCityValue`، `CityComboboxProps`، `ProvinceCitySelectProps`، `DataChange` و اینترفیس‌های `IranCities`، `IranCitiesGeo` و `IranCitiesHooks`.
528
+
529
+ > [!NOTE]
530
+ > استان‌ها، شهرها و آرایه‌های مشترک با `Object.freeze` قفل شده‌اند تا یک بخش برنامه نتواند تصادفاً داده‌ها را برای بخش دیگر خراب کند. برای تغییر، اول کپی بگیرید: `[...getCities('fars')].sort(...)`. `getProvincesList()`، `getCapitals()`، `getNeighborProvinces()` و توابع جستجو خودشان آرایه‌ی جدید برمی‌گردانند.
531
+
532
+ ## تطبیق متن چطور کار می‌کند
533
+
534
+ **نرمال‌سازی.** همه‌ی lookupها و جستجوها متن نرمال‌شده را مقایسه می‌کنند، پس این‌ها یکی حساب می‌شوند:
535
+
536
+ | یکسان | |
537
+ |---|---|
538
+ | `ی` `ي` `ى` `ئ` | `ک` `ك` |
539
+ | `ا` `آ` `أ` `إ` `ٱ` | `ه` `ة` `ۀ` |
540
+ | `و` `ؤ` | `۰۱۲` `٠١٢` `012` |
541
+
542
+ اعراب، کشیده (`ـ`)، نیم‌فاصله، فاصله، `-`، `_` و آپاستروف نادیده گرفته می‌شوند و بزرگی و کوچکی حروف مهم نیست.
543
+
544
+ **چیدمان کیبورد.** اگر جستجو نتیجه‌ای نداشته باشد، دوباره طوری جستجو می‌شود که انگار با چیدمان دیگر تایپ شده: `jivhk` ← `تهران` و `ساهقشظ` ← `shiraz`. با `{ keyboardFix: false }` خاموش می‌شود.
545
+
546
+ **ترتیب نتایج.** اول تطابق کامل، بعد شروع نام، بعد شامل‌بودن، و بعد (با `fuzzy`) فاصله‌ی ویرایشی. در امتیاز برابر، مراکز استان اول می‌آیند و بعد ترتیب الفبای فارسی.
547
+
548
+ **نام‌های تکراری.** ۲۵ نام شهر در بیش از یک استان وجود دارد. بدون استان، `getCity(name)` مرکز استان را برمی‌گرداند، وگرنه شهر شناخته‌شده‌تر را، وگرنه کوچک‌ترین id را. اگر انتخاب مهم است، با `isAmbiguousCityName(name)` چک کنید و استان را هم بدهید یا انتخاب را از `getCitiesByName(name)` به کاربر بسپارید.
549
+
550
+ **نام‌های جایگزین.** املاهای دیگر و نام‌های قدیمی به شهر درست می‌رسند: `طهران`، `teheran`، `esfahan`، `mashad`، `orumiyeh`، `صومعه سارا`، `حلاشی`.
280
551
 
281
- ## توسعه
552
+ ## داده‌ها و منابع
553
+
554
+ | | |
555
+ |---|---:|
556
+ | استان | ۳۱ |
557
+ | شهر | ۱۲۵۲ |
558
+ | شهر دارای مختصات | ۱۱۵۲ |
559
+ | ↳ اطمینان `high` | ۹۷۷ |
560
+ | ↳ اطمینان `medium` | ۹۸ |
561
+ | ↳ اطمینان `low` | ۷۷ |
562
+ | مرز مشترک بین استان‌ها | ۷۴ |
563
+
564
+ - **`high`**: یک منبع مستقل همان نقطه را تأیید می‌کند.
565
+ - **`medium`**: نام در GeoNames یکتاست و مکان شبیه شهر است، ولی تأیید مستقل ندارد.
566
+ - **`low`**: فقط تطابق نام در GeoNames. برای نمایش تقریبی مناسب است و خطای چند کیلومتری ممکن است.
567
+
568
+ همه‌ی مختصات در CI با مرز استان‌ها بررسی می‌شوند و هر ۱۱۵۲ نقطه داخل استان خودشان هستند. برای ۱۰۰ شهر باقی‌مانده `coordinates` برابر `undefined` است و هیچ مقدار حدسی گذاشته نشده ([فهرست و دلیل‌ها](./reports/coordinates-not-imported.csv)).
569
+
570
+ همسایه‌ها از روی مرز استان‌ها محاسبه شده‌اند (حداقل ۳ کیلومتر مرز مشترک) و مرزهای فعلی را نشان می‌دهند، از جمله انتقال شهرستان طبس از یزد به خراسان جنوبی در سال ۱۳۹۲.
571
+
572
+ idها هرگز تغییر نمی‌کنند و دوباره استفاده نمی‌شوند. slugهای تغییرکرده همچنان کار می‌کنند و همه‌ی تغییرات در `DATA_CHANGES` ثبت شده‌اند.
573
+
574
+ ## سازگاری
575
+
576
+ هر نسخه در CI روی این ترکیب‌ها تست می‌شود:
577
+
578
+ | | پشتیبانی |
579
+ |---|---|
580
+ | Node.js | 14، 16، 18، 20، 22، 24 (CommonJS و ES module) |
581
+ | Deno، Bun | نسخه‌های فعلی |
582
+ | TypeScript | 3.9 تا 7، با `moduleResolution` برابر `node`، `node16` یا `bundler` |
583
+ | React | 16.8 به بالا (تست‌شده با 17، 18 و 19) |
584
+ | Next.js | 14، 15، 16 (App Router؛ Turbopack و webpack) |
585
+ | Zod، Valibot | Zod 3.25 به بالا و 4، Valibot 1 |
586
+
587
+ هر bundler که فیلد `exports` را بشناسد (Vite، webpack، Rollup، Rolldown، esbuild، Turbopack و ...) می‌تواند از پکیج استفاده کند.
588
+
589
+ ## مشارکت
590
+
591
+ ارزشمندترین مشارکت، اصلاح داده است؛ لطفاً منبع را هم بنویسید. [CONTRIBUTING.fa.md](./CONTRIBUTING.fa.md) را ببینید.
282
592
 
283
593
  ```bash
284
594
  npm install
285
- npm run build # خروجی dist/ (cjs + esm + d.ts)
286
- npm run typecheck
287
- npm run validate # اعتبارسنجی یکپارچگی data.json (id، slug، مرکز استان، کد تلفن، مختصات)
288
- npm test # validate + typecheck
595
+ npm test # اعتبارسنجی داده، TypeScript 7، Oxlint، Vitest
596
+ npm run build # tsdown (Rolldown + Oxc)
597
+ npm run test:dist # smoke test فایل‌های build شده
289
598
  ```
290
599
 
291
- > **مختصات:** برای ۳۷۰ شهر (۳۱ مرکز استان + عمدتاً مراکز شهرستان) موجود است و فقط وقتی ثبت شده که حداقل دو منبع مستقل روی آن توافق داشته‌اند (جزئیات در `reports/`). برای بقیه `coordinates` برابر `undefined` است و هیچ مقدار تقریبی جایگزین نمی‌شود. منبع داده: GeoNames (CC BY 4.0)، فایل `NOTICE.md`.
600
+ برای انتشار نسخه‌ی جدید، `version` را در `package.json` و `CHANGELOG.md` به‌روز کنید و بعد:
601
+
602
+ ```bash
603
+ npm publish --access public
604
+ ```
605
+
606
+ `npm publish` قبل از انتشار همه‌ی بررسی‌ها را اجرا می‌کند (تست‌ها، build، smoke test، lint پکیج و بودجه‌ی حجم) و اگر یکی خطا بدهد، انتشار متوقف می‌شود.
607
+
608
+ ## مجوز و منابع
609
+
610
+ | | منبع | مجوز |
611
+ |---|---|---|
612
+ | کد | همین پروژه | [MIT](./LICENSE) |
613
+ | مختصات | [GeoNames](https://www.geonames.org) | CC BY 4.0 |
614
+ | مرزها و همسایه‌ها | [geoBoundaries](https://www.geoboundaries.org) / © OpenStreetMap contributors | ODbL 1.0 |
615
+
616
+ فیلد license پکیج `MIT AND CC-BY-4.0 AND ODbL-1.0` است. ODbL فقط وقتی اعمال می‌شود که خود داده‌ی مرزها را بازنشر کنید؛ [`NOTICE.md`](./NOTICE.md) را ببینید.
292
617
 
293
- ## لایسنس
618
+ <div align="center">
619
+ <sub>ساخته‌شده با ❤️ برای توسعه‌دهنده‌های فارسی‌زبان · <a href="./CHANGELOG.md">Changelog</a> · <a href="./MIGRATION.fa.md">راهنمای مهاجرت</a> · <a href="https://mjavadsf.github.io/Iran-Cities/fa/">مستندات</a> · <a href="https://github.com/MJavadSF/Iran-Cities/issues">Issues</a></sub>
620
+ </div>
294
621
 
295
- MIT
622
+ </div>