@isofh/chuyen-doi-dia-chi-2-cap 1.0.0 → 1.0.2

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 (4) hide show
  1. package/README.md +49 -23
  2. package/dataXa.json +12 -11
  3. package/index.js +98 -20
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @isofh/chuyen-doi-dia-chi-2-cap
2
2
 
3
- Package chuẩn hóa địa chỉ hành chính sang format 2 cấp mới, kèm theo bộ dữ liệu `dataXa.json`.
3
+ Package chuẩn hóa địa chỉ hành chính sang format 2 cấp, kèm theo bộ dữ liệu `dataXa.json`.
4
4
 
5
5
  ## Mục tiêu
6
6
 
7
- - đóng gói `dataXa.json` để thể dùng lại giữa nhiều dự án
8
- - tách logic `convertDiaChiMoi` khỏi component UI
7
+ - cung cấp bộ dữ liệu hành chính dùng cho chuẩn hóa địa chỉ
8
+ - cung cấp các hàm chuẩn hóa, kiểm tra xã hợp lệ và bóc địa chỉ từ QR CCCD
9
9
  - đóng gói thành npm package để dùng lại giữa nhiều dự án
10
10
 
11
11
  ## Cài đặt
@@ -39,19 +39,22 @@ const {
39
39
  extractAndConvertCccdAddress,
40
40
  } = require("@isofh/chuyen-doi-dia-chi-2-cap");
41
41
 
42
- const output = convertDiaChiMoi("Xuân Thành, Xuân Lộc, Đồng Nai");
43
- console.log(output);
44
-
45
42
  const verifyResult = verifyXaName("Xuân Thành", "Đồng Nai");
46
43
  console.log(verifyResult.isValid);
47
44
 
48
45
  const splitResult = splitAddressByValidXa("Phú Thạch, Phú Trung, Đồng Nai", "Đồng Nai");
49
46
  console.log(splitResult);
50
47
 
51
- const cccdResult = extractAndConvertCccdAddress(
52
- "079306006606||Vũ Ngọc Như Lan|27102006|Nữ|19/20 Tổ 71, Khu, Phố 6, Phường 06, Tân Bình, TP.Hồ Chí Minh|26012022"
53
- );
54
- console.log(cccdResult);
48
+ (async () => {
49
+ const output = await convertDiaChiMoi("Xuân Thành, Xuân Lộc, Đồng Nai");
50
+ console.log(output);
51
+
52
+ const cccdResult = await extractAndConvertCccdAddress(
53
+ "079306006606||Vũ Ngọc Như Lan|27102006|Nữ|19/20 Tổ 71, Khu, Phố 6, Phường 06, Tân Bình, TP.Hồ Chí Minh|26012022"
54
+ );
55
+ console.log(cccdResult.soNha);
56
+ console.log(cccdResult.diaChi);
57
+ })();
55
58
  ```
56
59
 
57
60
  ## Input hỗ trợ
@@ -75,17 +78,20 @@ const fullAddress = "4A Lê Thánh Tông, Cửa Nam, Hà Nội";
75
78
  // Caller tự chuẩn hóa/tách phần hành chính trước
76
79
  const administrativeAddress = "Cửa Nam, Hà Nội";
77
80
 
78
- const output = convertDiaChiMoi(administrativeAddress);
81
+ (async () => {
82
+ const output = await convertDiaChiMoi(administrativeAddress);
83
+ console.log(output);
84
+ })();
79
85
  ```
80
86
 
81
87
  ## Nguyên tắc xử lý
82
88
 
83
- Logic hiện tại bám theo bộ quy tắc chuyển đổi địa chỉ đang dùng trong hệ thống HIS.
89
+ Logic hiện tại bám theo bộ quy tắc chuẩn hóa địa chỉ hành chính 2 cấp.
84
90
 
85
91
  Thứ tự xử lý chính:
86
92
 
87
93
  1. Match tỉnh trước bằng tên mới hoặc tên cũ trong `dsTinh`.
88
- 2. Nếu input là 3 vế thì ưu tiên hiểu là ` cũ, huyện cũ, tỉnh cũ` và chỉ giữ xã + tỉnh để đưa về 2 cấp.
94
+ 2. Nếu input là 3 vế thì ưu tiên hiểu là `xã/phường, huyện/quận, tỉnh/thành` và chỉ giữ xã + tỉnh để đưa về 2 cấp.
89
95
  3. Tìm xã/phường theo thứ tự từ chặt đến lỏng:
90
96
  - match chính xác tên xã hiện tại
91
97
  - match trong `satNhapTu`
@@ -97,9 +103,11 @@ Thứ tự xử lý chính:
97
103
  ```js
98
104
  const { convertDiaChiMoi } = require("@isofh/chuyen-doi-dia-chi-2-cap");
99
105
 
100
- console.log(convertDiaChiMoi("Xuân Thành, Xuân Lộc, Đồng Nai"));
101
- console.log(convertDiaChiMoi("Cửa Nam, Nội"));
102
- console.log(convertDiaChiMoi(["Phú Thạch", "Phú Trung", "Đồng Nai"]));
106
+ (async () => {
107
+ console.log(await convertDiaChiMoi("Xuân Thành, Xuân Lộc, Đồng Nai"));
108
+ console.log(await convertDiaChiMoi("Cửa Nam, Nội"));
109
+ console.log(await convertDiaChiMoi(["Phú Thạch", "Phú Trung", "Đồng Nai"]));
110
+ })();
103
111
  ```
104
112
 
105
113
  ## Verify tên xã
@@ -155,11 +163,11 @@ globalThis.splitAddressByValidXa;
155
163
  globalThis.extractAndConvertCccdAddress;
156
164
  ```
157
165
 
158
- Mục đích là để một package khác như `mainam-react-native-string-utils` có thể tận dụng runtime function này mà không phải tạo dependency ngược.
166
+ Mục đích là để caller có thể tận dụng các helper này ở runtime mà không cần tự truyền tay qua nhiều lớp.
159
167
 
160
168
  ## Trích xuất địa chỉ căn cước
161
169
 
162
- `parseCccdQrString` dùng để parse chuỗi QR CCCD dạng đang được app HIS quét vào, ví dụ:
170
+ `parseCccdQrString` dùng để parse chuỗi QR CCCD dạng pipe, ví dụ:
163
171
 
164
172
  ```txt
165
173
  079306006606||Vũ Ngọc Như Lan|27102006|Nữ|19/20 Tổ 71, Khu, Phố 6, Phường 06, Tân Bình, TP.Hồ Chí Minh|26012022
@@ -183,21 +191,39 @@ console.log(extractCccdAddressText("079306006606||Vũ Ngọc Như Lan|27102006|N
183
191
  - parse theo logic `toAddress`
184
192
  - convert phần địa chỉ hành chính về format mới bằng `convertDiaChiMoi`
185
193
  - trả về object đúng shape của `String.prototype.toAddress`
194
+ - giữ lại `soNha` ở top-level output
195
+ - bổ sung `data.soNha` và `data.diaChiGoc` để caller có thể dùng lại dữ liệu trước và sau bước convert
186
196
 
187
197
  Ví dụ:
188
198
 
189
199
  ```js
190
200
  const { extractAndConvertCccdAddress } = require("@isofh/chuyen-doi-dia-chi-2-cap");
191
201
 
192
- console.log(
193
- extractAndConvertCccdAddress(
194
- "079306006606||Vũ Ngọc Như Lan|27102006|Nữ|19/20 Tổ 71, Khu, Phố 6, Phường 06, Tân Bình, TP.Hồ Chí Minh|26012022"
195
- )
196
- );
202
+ (async () => {
203
+ console.log(
204
+ await extractAndConvertCccdAddress(
205
+ "079306006606||Vũ Ngọc Như Lan|27102006|Nữ|19/20 Tổ 71, Khu, Phố 6, Phường 06, Tân Bình, TP.Hồ Chí Minh|26012022"
206
+ )
207
+ );
208
+ })();
197
209
  ```
198
210
 
199
211
  Nếu không truyền `ngayCap`, hàm sẽ tự lấy ngày cấp từ field QR CCCD nếu chuỗi input đúng format pipe.
200
212
 
213
+ Ví dụ output rút gọn:
214
+
215
+ ```js
216
+ {
217
+ soNha: "19/20 Tổ 71, Khu, Phố 6",
218
+ diaChi: "Tân Hòa, Hồ Chí Minh",
219
+ data: {
220
+ soNha: "19/20 Tổ 71, Khu, Phố 6",
221
+ diaChiGoc: "06, Tân Bình, TP.Hồ Chí Minh",
222
+ convertedDiaChi: "Tân Hòa, Hồ Chí Minh"
223
+ }
224
+ }
225
+ ```
226
+
201
227
  ## Tách số nhà dựa trên xã hợp lệ
202
228
 
203
229
  Ý tưởng của hàm `splitAddressByValidXa` là:
package/dataXa.json CHANGED
@@ -23849,7 +23849,7 @@
23849
23849
  "huyen": "xuanloc",
23850
23850
  "satNhapTu": [
23851
23851
  { "ten": "Suối Cao", "timKiem": "suoicao" },
23852
- { "ten": "Xuân Thành", "timKiem": "xuanthanh", "huyen":"xuanloc" }
23852
+ { "ten": "Xuân Thành", "timKiem": "xuanthanh", "huyen": "xuanloc" }
23853
23853
  ]
23854
23854
  },
23855
23855
  {
@@ -24313,6 +24313,7 @@
24313
24313
  "timKiem": "hochiminh",
24314
24314
  "dsTinh": [
24315
24315
  { "ten": "Bà Rịa - Vũng Tàu", "timKiem": "baria-vungtau" },
24316
+ { "ten": "BR-VT", "timKiem": "br-vt" },
24316
24317
  { "ten": "Bình Dương", "timKiem": "binhduong" }
24317
24318
  ],
24318
24319
  "dsXa": [
@@ -26887,8 +26888,8 @@
26887
26888
  "ten": "Mỹ Tho",
26888
26889
  "timKiem": "mytho",
26889
26890
  "satNhapTu": [
26890
- { "ten": "1", "timKiem": "1" },
26891
- { "ten": "2", "timKiem": "2" },
26891
+ { "ten": "1", "timKiem": "1", "huyen": "mytho" },
26892
+ { "ten": "2", "timKiem": "2", "huyen": "mytho" },
26892
26893
  { "ten": "Tân Long", "timKiem": "tanlong" }
26893
26894
  ]
26894
26895
  },
@@ -26914,7 +26915,7 @@
26914
26915
  "ten": "Thới Sơn",
26915
26916
  "timKiem": "thoison",
26916
26917
  "satNhapTu": [
26917
- { "ten": "6", "timKiem": "6" },
26918
+ { "ten": "6", "timKiem": "6", "huyen": "mytho" },
26918
26919
  { "ten": "Thới Sơn", "timKiem": "thoison" }
26919
26920
  ]
26920
26921
  },
@@ -26922,7 +26923,7 @@
26922
26923
  "ten": "Trung An",
26923
26924
  "timKiem": "trungan",
26924
26925
  "satNhapTu": [
26925
- { "ten": "10", "timKiem": "10" },
26926
+ { "ten": "10", "timKiem": "10", "huyen": "mytho" },
26926
26927
  { "ten": "Trung An", "timKiem": "trungan" },
26927
26928
  { "ten": "Phước Thạnh", "timKiem": "phuocthanh" }
26928
26929
  ]
@@ -26931,8 +26932,8 @@
26931
26932
  "ten": "Gò Công",
26932
26933
  "timKiem": "gocong",
26933
26934
  "satNhapTu": [
26934
- { "ten": "1", "timKiem": "1" },
26935
- { "ten": "5", "timKiem": "5" },
26935
+ { "ten": "1", "timKiem": "1", "huyen": "gocong" },
26936
+ { "ten": "5", "timKiem": "5", "huyen": "gocong" },
26936
26937
  { "ten": "Long Hòa", "timKiem": "longhoa" }
26937
26938
  ]
26938
26939
  },
@@ -26940,7 +26941,7 @@
26940
26941
  "ten": "Long Thuận",
26941
26942
  "timKiem": "longthuan",
26942
26943
  "satNhapTu": [
26943
- { "ten": "2", "timKiem": "2" },
26944
+ { "ten": "2", "timKiem": "2", "huyen": "gocong" },
26944
26945
  { "ten": "Long Thuận", "timKiem": "longthuan" }
26945
26946
  ]
26946
26947
  },
@@ -26965,8 +26966,8 @@
26965
26966
  "ten": "Mỹ Phước Tây",
26966
26967
  "timKiem": "myphuoctay",
26967
26968
  "satNhapTu": [
26968
- { "ten": "1", "timKiem": "1" },
26969
- { "ten": "3", "timKiem": "3" },
26969
+ { "ten": "1", "timKiem": "1", "huyen": "cailay" },
26970
+ { "ten": "3", "timKiem": "3", "huyen": "cailay" },
26970
26971
  { "ten": "Mỹ Hạnh Trung", "timKiem": "myhanhtrung" },
26971
26972
  { "ten": "Mỹ Phước Tây", "timKiem": "myphuoctay" }
26972
26973
  ]
@@ -26975,7 +26976,7 @@
26975
26976
  "ten": "Thanh Hòa",
26976
26977
  "timKiem": "thanhhoa",
26977
26978
  "satNhapTu": [
26978
- { "ten": "2", "timKiem": "2" },
26979
+ { "ten": "2", "timKiem": "2", "huyen": "cailay" },
26979
26980
  { "ten": "Tân Bình", "timKiem": "tanbinh" },
26980
26981
  { "ten": "Thanh Hòa", "timKiem": "thanhhoa" }
26981
26982
  ]
package/index.js CHANGED
@@ -1,6 +1,39 @@
1
1
  require("mainam-react-native-string-utils");
2
2
 
3
- const dataXa = require("./dataXa.json");
3
+ let dataXa = null;
4
+ let dataXaPromise = null;
5
+
6
+ // Resolve global runtime để hỗ trợ các hook override giống contract gốc ở repo cũ.
7
+ const getGlobalTarget = () =>
8
+ typeof globalThis !== "undefined"
9
+ ? globalThis
10
+ : typeof window !== "undefined"
11
+ ? window
12
+ : typeof global !== "undefined"
13
+ ? global
14
+ : null;
15
+
16
+ // Nạp dataset hành chính ở thời điểm cần dùng thay vì load ngay khi require package.
17
+ const getDataXaSync = () => {
18
+ if (!dataXa) {
19
+ dataXa = require("./dataXa.json");
20
+ }
21
+
22
+ return dataXa;
23
+ };
24
+
25
+ // Giữ contract async của hàm gốc để caller có thể await trước khi convert.
26
+ const loadDataXa = async (customDataXa) => {
27
+ if (customDataXa) {
28
+ return customDataXa;
29
+ }
30
+
31
+ if (!dataXaPromise) {
32
+ dataXaPromise = Promise.resolve().then(() => getDataXaSync());
33
+ }
34
+
35
+ return dataXaPromise;
36
+ };
4
37
 
5
38
  // Dùng helper của string-utils để tạo key tìm kiếm không dấu, không khoảng trắng.
6
39
  const createUniqueText = (text = "") => `${text ?? ""}`.createUniqueText();
@@ -164,7 +197,7 @@ const dedupeMatches = (matches = []) => {
164
197
  // Kiểm tra một tên xã có hợp lệ trong phạm vi tỉnh đích hay không.
165
198
  const verifyXaName = (tenXa, tinhOrOptions, maybeOptions) => {
166
199
  const { tinh, options } = getVerifyOptions(tinhOrOptions, maybeOptions);
167
- const sourceDataXa = options.dataXa || dataXa;
200
+ const sourceDataXa = options.dataXa || getDataXaSync();
168
201
  const normalizedXa = normalizeAdministrativeCode(tenXa);
169
202
  const normalizedTinh = normalizeAdministrativeCode(tinh);
170
203
  const matchedProvinces = getProvinceCandidates(tinh, sourceDataXa);
@@ -220,7 +253,7 @@ const verifyXaName = (tenXa, tinhOrOptions, maybeOptions) => {
220
253
  // Tìm xã hợp lệ đầu tiên trong chuỗi địa chỉ để tách các vế đứng trước thành `soNha`.
221
254
  const splitAddressByValidXa = (input, tinhOrOptions, maybeOptions) => {
222
255
  const { tinh, options } = getVerifyOptions(tinhOrOptions, maybeOptions);
223
- const sourceDataXa = options.dataXa || dataXa;
256
+ const sourceDataXa = options.dataXa || getDataXaSync();
224
257
  const parts = splitAddressParts(input);
225
258
 
226
259
  if (!parts.length) {
@@ -283,18 +316,21 @@ const splitAddressByValidXa = (input, tinhOrOptions, maybeOptions) => {
283
316
  };
284
317
 
285
318
  // Bóc địa chỉ từ text căn cước rồi trả về output theo đúng contract của `String.prototype.toAddress`.
286
- const extractAndConvertCccdAddress = (input, ngayCap, options = {}) => {
319
+ const extractAndConvertCccdAddress = async (input, ngayCap, options = {}) => {
287
320
  const sourceText = `${input ?? ""}`;
288
321
  const parsedQr = parseCccdQrString(sourceText);
289
322
  const extractedText = extractCccdAddressText(sourceText);
290
323
  const issueDate = normalizeCccdIssueDate(ngayCap || parsedQr?.ngayCap);
291
324
  const parsedAddress = extractedText.toAddress({ ngayCap: issueDate, verifyXaName });
325
+ const soNha = parsedAddress?.soNha || "";
326
+ const diaChiGoc = parsedAddress?.diaChi || "";
292
327
  const convertedDiaChi = parsedAddress?.diaChi
293
- ? convertDiaChiMoi(parsedAddress.diaChi, options)
328
+ ? await convertDiaChiMoi(parsedAddress.diaChi, options)
294
329
  : "";
295
330
 
296
331
  return {
297
332
  ...(parsedAddress || {}),
333
+ soNha,
298
334
  diaChi: convertedDiaChi,
299
335
  data: {
300
336
  ...(parsedAddress?.data || {}),
@@ -302,6 +338,8 @@ const extractAndConvertCccdAddress = (input, ngayCap, options = {}) => {
302
338
  cccdFields: parsedQr?.fields || [],
303
339
  extractedText,
304
340
  issueDate,
341
+ soNha,
342
+ diaChiGoc,
305
343
  convertedDiaChi,
306
344
  },
307
345
  };
@@ -310,16 +348,54 @@ const extractAndConvertCccdAddress = (input, ngayCap, options = {}) => {
310
348
  /**
311
349
  * Nghiệp vụ chuẩn hóa địa chỉ hành chính cũ về format 2 cấp mới.
312
350
  *
313
- * Logic phần này được ra từ repo cũ, chỉ thay phần lấy dữ liệu sang `dataXa` local
314
- * giữ khả năng override bằng `options.dataXa` để tiện test/publish package.
351
+ * Đầu vào chấp nhận cả chuỗi lẫn mảng, thường gặp các dạng:
352
+ * - `Xã/Phường, Tỉnh/Thành`
353
+ * - `Xã/Phường cũ, Quận/Huyện cũ, Tỉnh/Thành cũ`
354
+ * - Một số case chỉ có 1 vế nhưng thực chất là tên xã/phường.
355
+ *
356
+ * Quy tắc chung:
357
+ * - Nếu đầu vào là 3 cấp thì chỉ giữ xã và tỉnh để đưa hệ thống về format 2 cấp.
358
+ * - Tỉnh được match trước, hỗ trợ cả tên tỉnh mới lẫn tỉnh cũ trong `dsTinh`.
359
+ * - Nếu không match được tỉnh thì trả nguyên dữ liệu cũ để tránh đổi sai địa chỉ.
360
+ * - Sau khi xác định được tỉnh, xã/phường sẽ được tìm theo thứ tự ưu tiên từ chặt đến lỏng.
361
+ *
362
+ * Thứ tự resolve xã/phường:
363
+ * 1. Case đặc biệt chỉ có 1 vế nhưng thực chất là tên xã trong tỉnh vừa match.
364
+ * 2. Match chính xác tên xã/phường hiện tại, ưu tiên cùng huyện cũ nếu đầu vào có huyện.
365
+ * 3. Match chính xác trong danh sách `satNhapTu`, vẫn ưu tiên cùng huyện cũ nếu có.
366
+ * 4. Nếu chưa ra kết quả thì match gần đúng trong `satNhapTu` và bỏ điều kiện huyện.
367
+ * 5. Cuối cùng mới nới lỏng sang match chính xác tên xã/phường hiện tại nhưng bỏ điều kiện huyện.
368
+ *
369
+ * Kết quả trả về luôn là `Xã/Phường mới, Tỉnh/Thành mới`.
370
+ * Huyện chỉ dùng để phân biệt khi tìm kiếm, không còn nằm trong output cuối.
371
+ *
372
+ * Lưu ý về contract hiện tại của package:
373
+ * - Hàm giữ dạng `async` để caller có thể `await` giống luồng gốc.
374
+ * - Dữ liệu hành chính được nạp lazy qua `loadDataXa()` ở thời điểm cần dùng.
375
+ * - Nếu có `window.modifyDataXa`, dữ liệu sẽ được đi qua hook này trước khi chạy thuật toán mặc định.
376
+ * - Nếu có `window.convertDiaChiMoi`, hàm sẽ ưu tiên giao toàn bộ việc convert cho hook runtime này.
315
377
  */
316
- const convertDiaChiMoi = (diaChi2CapCu, options = {}) => {
317
- const sourceDataXa = options.dataXa || dataXa;
378
+ const convertDiaChiMoi = async (diaChi2CapCu, options = {}) => {
379
+ const globalTarget = getGlobalTarget();
318
380
 
319
- if (typeof diaChi2CapCu === "string" && diaChi2CapCu.length <= 6) {
381
+ if (
382
+ !globalTarget?.convertDiaChiMoi &&
383
+ typeof diaChi2CapCu === "string" &&
384
+ diaChi2CapCu.length <= 6
385
+ ) {
320
386
  return diaChi2CapCu;
321
387
  }
322
388
 
389
+ let sourceDataXa = await loadDataXa(options.dataXa);
390
+
391
+ if (globalTarget?.modifyDataXa) {
392
+ sourceDataXa = globalTarget.modifyDataXa(sourceDataXa);
393
+ }
394
+
395
+ if (globalTarget?.convertDiaChiMoi) {
396
+ return await Promise.resolve(globalTarget.convertDiaChiMoi(diaChi2CapCu, sourceDataXa));
397
+ }
398
+
323
399
  let diaChi = [];
324
400
  const arr = splitAddressParts(diaChi2CapCu);
325
401
 
@@ -430,14 +506,7 @@ const convertDiaChiMoi = (diaChi2CapCu, options = {}) => {
430
506
  };
431
507
 
432
508
  // Mount helper lên global để các package khác có thể dùng runtime mà không cần dependency ngược.
433
- const globalTarget =
434
- typeof globalThis !== "undefined"
435
- ? globalThis
436
- : typeof window !== "undefined"
437
- ? window
438
- : typeof global !== "undefined"
439
- ? global
440
- : null;
509
+ const globalTarget = getGlobalTarget();
441
510
 
442
511
  if (globalTarget) {
443
512
  globalTarget.verifyXaName = verifyXaName;
@@ -445,11 +514,11 @@ if (globalTarget) {
445
514
  globalTarget.extractAndConvertCccdAddress = extractAndConvertCccdAddress;
446
515
  }
447
516
 
448
- module.exports = {
449
- dataXa,
517
+ const exported = {
450
518
  createUniqueText,
451
519
  stripAdministrativePrefix,
452
520
  normalizeAdministrativeCode,
521
+ loadDataXa,
453
522
  convertDiaChiMoi,
454
523
  verifyXaName,
455
524
  splitAddressByValidXa,
@@ -458,3 +527,12 @@ module.exports = {
458
527
  extractCccdAddressText,
459
528
  extractAndConvertCccdAddress,
460
529
  };
530
+
531
+ Object.defineProperty(exported, "dataXa", {
532
+ enumerable: true,
533
+ get() {
534
+ return getDataXaSync();
535
+ },
536
+ });
537
+
538
+ module.exports = exported;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isofh/chuyen-doi-dia-chi-2-cap",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Chuan hoa dia chi hanh chinh cu sang format 2 cap moi",
5
5
  "main": "index.js",
6
6
  "files": [