VietnamAddress 1.0.0
dotnet add package VietnamAddress --version 1.0.0
NuGet\Install-Package VietnamAddress -Version 1.0.0
<PackageReference Include="VietnamAddress" Version="1.0.0" />
<PackageVersion Include="VietnamAddress" Version="1.0.0" />
<PackageReference Include="VietnamAddress" />
paket add VietnamAddress --version 1.0.0
#r "nuget: VietnamAddress, 1.0.0"
#:package VietnamAddress@1.0.0
#addin nuget:?package=VietnamAddress&version=1.0.0
#tool nuget:?package=VietnamAddress&version=1.0.0
<div align="center">
VietnamAddress
Dữ liệu đơn vị hành chính Việt Nam cho .NET — chạy hoàn toàn offline.
<img src="https://raw.githubusercontent.com/webuild-community/badge/master/svg/made.svg" alt="Made in Vietnam" />
</div>
Không API. Không database. Không máy chủ. Toàn bộ dữ liệu nén gzip và nhúng thẳng trong dll — 570 KB, một package duy nhất.
Bao gồm 34 tỉnh thành / 3.321 phường xã theo mô hình 2 cấp (hiệu lực từ 01/07/2025), 63 tỉnh / 696 quận huyện / 10.035 phường xã theo địa giới cũ, bảng ánh xạ cũ ⇄ mới để quy đổi địa chỉ sau sáp nhập, và toạ độ tâm của mọi đơn vị hiện hành.
Nếu thấy hữu ích, để lại một ⭐ nhé.
dotnet add package VietnamAddress
Tổng quan
Thư viện này không thuộc Cục Thống kê hay bất kỳ cơ quan nhà nước nào. Dữ liệu được kéo thẳng từ nguồn chính thức của nhà nước, rồi chuẩn hoá thành bảng phẳng, nén gzip và bọc bằng một API C# type-safe.
| Dữ liệu | Nguồn |
|---|---|
| Danh mục 2 cấp hiện hành | Cục Thống kê — SOAP DanhMucTinh / DanhMucPhuongXa |
| Danh mục 3 cấp cũ | cùng API, tham số DenNgay=30/06/2025 |
| Bảng ánh xạ sáp nhập | Bảng chuyển đổi ĐVHC mới ⇄ cũ chính thức (tools/sources/) — ánh xạ mã ↔ mã |
Toàn bộ quy trình nằm trong tools/build_dataset.py — công khai và tái tạo được. Không phụ thuộc dataset bên thứ ba nào.
Vì sao lại làm hẳn một thư viện thay vì gọi API bên thứ ba: các API địa chỉ miễn phí của Việt Nam hay chết. Dự án gốc của tác giả từng phụ thuộc một dịch vụ như vậy, và khi domain của nó ngừng hoạt động thì mọi tính năng liên quan âm thầm trả về rỗng — backend vẫn chạy, không crash, chỉ là dữ liệu biến mất. Dữ liệu hành chính gần như không đổi (vài năm một nghị quyết), nên nhét thẳng vào package là hợp lý hơn hẳn việc gọi mạng.
Điểm khác biệt
| Offline tuyệt đối | Không có một request mạng nào. Không thể "chết" như API bên thứ ba |
| Có địa giới cũ | Địa chỉ cũ đã lưu trong DB vẫn tra cứu được, dù đơn vị đó không còn tồn tại |
| Quy đổi cũ ⇄ mới | Ánh xạ mã ↔ mã từ bảng chuyển đổi chính thức, kèm cờ nhập toàn bộ / một phần |
| Đọc được địa chỉ CŨ | Chuỗi gõ tay theo địa giới cũ → tự quy đổi sang đơn vị mới, kể cả khi tỉnh đã bị xoá sổ |
| Tìm không dấu | hcm, ho chi minh, Hồ Chí Minh, 79, Li Son/Lý Sơn — gõ kiểu gì cũng ra |
Phiên bản
| Mô tả | Mốc dữ liệu | Phiên bản |
|---|---|---|
| Danh mục 2 cấp + địa giới cũ + ánh xạ sáp nhập + parser + toạ độ tâm + ghép địa chỉ + CLI | Hiện hành / DenNgay=30/06/2025 |
v1.0.0 |
Lịch sử thay đổi đầy đủ: CHANGELOG.md.
Chính phủ có thể ban hành nghị quyết thay đổi đơn vị hành chính bất cứ lúc nào. Chạy lại python3 tools/build_dataset.py --write là có bộ dữ liệu mới — kéo thẳng từ nguồn nhà nước, không chờ ai cả.
Cài đặt
dotnet add package VietnamAddress
Không cần tạo database, không cần chuỗi kết nối, không cần cấu hình. Dùng luôn:
using VietnamAddress.Json;
var store = VietnamAddressStore.Instance;
var provinces = store.GetProvinces(); // 34 tỉnh thành
var wards = store.GetWardsByProvince("79"); // phường xã của TP.HCM
var hcm = store.GetProvince("79"); // null nếu mã không tồn tại
Với ASP.NET Core:
builder.Services.AddVietnamAddress(); // singleton
public class AddressController(IVietnamAddressStore store) : ControllerBase
{
[HttpGet("provinces")]
public IActionResult GetProvinces() => Ok(store.GetProvinces());
[HttpGet("provinces/{code}/wards")]
public IActionResult GetWards(string code) => Ok(store.GetWardsByProvince(code));
}
Cấu trúc namespace
Tất cả nằm trong một package duy nhất — cài VietnamAddress là có đủ, không có gói con nào phải cài thêm.
| Namespace | Nội dung |
|---|---|
VietnamAddress.Core |
Model, interface |
VietnamAddress.Json |
Dữ liệu nhúng, store, bảng ánh xạ sáp nhập |
VietnamAddress.Search |
Tìm không dấu, viết tắt, chính tả i/y |
VietnamAddress.Parser |
Tách chuỗi địa chỉ, đọc được cả địa giới cũ |
VietnamAddress.Format |
Ghép mã → chuỗi địa chỉ, cả dạng mới lẫn dạng cũ |
VietnamAddress.Geo |
Toạ độ tâm, khoảng cách, phường gần nhất |
VietnamAddress.AspNetCore |
AddVietnamAddress() |
Target net8.0 và net9.0. Thread-safe, đăng ký singleton. Mỗi bảng chỉ giải nén ở lần truy cập đầu tiên — app chỉ dùng dữ liệu 2 cấp hiện hành sẽ không phải trả chi phí nạp 10.035 phường xã cũ.
Trimming và Native AOT
Dùng được với PublishTrimmed, PublishAot và Blazor WebAssembly. Metadata JSON sinh lúc biên dịch (JsonSerializerContext), không reflection.
Bỏ dấu tiếng Việt tra bảng tường minh chứ không qua ICU — chủ ý. Khi app bật InvariantGlobalization (mặc định khuyến nghị cho container, gần như bắt buộc với Native AOT), string.Normalize không ném lỗi mà lặng lẽ trả về chuỗi y nguyên, không tách dấu. Thư viện nào dựa vào nó sẽ trượt mọi so khớp và trả về rỗng — sai mà không báo. Thư viện này chạy giống hệt nhau ở cả hai chế độ.
Mô hình dữ liệu
Province — tỉnh / thành phố trực thuộc trung ương
Đơn vị hành chính cấp 1, gồm 34 tỉnh và thành phố trực thuộc trung ương.
| Thuộc tính | Kiểu | Ý nghĩa |
|---|---|---|
Code |
string |
Mã đơn vị chính thức do chính phủ quy ước. Khoá chính |
Name |
string |
Tên tiếng Việt |
NameEn |
string |
Tên tiếng Anh — tự sinh bằng bỏ dấu (nguồn nhà nước bỏ trống cột này) |
FullName |
string |
Tên tiếng Việt đầy đủ, kèm loại đơn vị |
FullNameEn |
string |
Tên tiếng Anh đầy đủ |
CodeName |
string |
Tên mã chữ thường xếp gạch — dùng làm khoá tìm kiếm không dấu |
UnitId |
int |
Trỏ tới AdministrativeUnit.Id |
| Code | Name | NameEn | FullName | CodeName | UnitId |
|---|---|---|---|---|---|
| 01 | Hà Nội | Ha Noi | Thành phố Hà Nội | ha_noi | 1 |
| 56 | Khánh Hòa | Khanh Hoa | Tỉnh Khánh Hòa | khanh_hoa | 2 |
| 79 | Hồ Chí Minh | Ho Chi Minh | Thành phố Hồ Chí Minh | ho_chi_minh | 1 |
| 96 | Cà Mau | Ca Mau | Tỉnh Cà Mau | ca_mau | 2 |
Ward — phường / xã / đặc khu
Đơn vị hành chính cấp 2, gồm 3.321 phường, xã và đặc khu. Trực thuộc thẳng tỉnh — không còn cấp quận/huyện ở giữa.
| Thuộc tính | Kiểu | Ý nghĩa |
|---|---|---|
Code |
string |
Mã đơn vị chính thức. Khoá chính |
Name … CodeName |
string |
Như Province |
ProvinceCode |
string |
Trỏ tới Province.Code |
UnitId |
int |
Trỏ tới AdministrativeUnit.Id |
| Code | Name | FullName | CodeName | ProvinceCode | UnitId |
|---|---|---|---|---|---|
| 25747 | Thủ Dầu Một | Phường Thủ Dầu Một | thu_dau_mot | 79 | 3 |
| 25750 | Phú Lợi | Phường Phú Lợi | phu_loi | 79 | 3 |
| 25760 | Bình Dương | Phường Bình Dương | binh_duong | 79 | 3 |
| 25768 | Phú An | Phường Phú An | phu_an | 79 | 3 |
⚠️ Phường xã trùng tên rất nhiều trên cả nước. Luôn truyền
provinceCodekhi tìm kiếm.
AdministrativeUnit — loại đơn vị hành chính
| Id | ShortName | FullName | ShortNameEn | FullNameEn |
|---|---|---|---|---|
| 1 | Thành phố | Thành phố trực thuộc trung ương | City | Municipality |
| 2 | Tỉnh | Tỉnh | Province | Province |
| 3 | Phường | Phường | Ward | Ward |
| 4 | Xã | Xã | Commune | Commune |
| 5 | Đặc khu | Đặc khu tại hải đảo | Special zone | Island special zone |
| 6–10 | Quận, Huyện, Thị xã, Thành phố thuộc tỉnh, Thị trấn | (chỉ dùng cho địa giới cũ) |
LegacyProvince / LegacyDistrict / LegacyWard — địa giới cũ
Mô hình 3 cấp trước 01/07/2025: 63 tỉnh → 696 quận huyện → 10.035 phường xã.
store.GetLegacyProvinces(); // 63 tỉnh
store.GetLegacyDistrictsByProvince("79"); // quận huyện của TP.HCM cũ
store.GetLegacyWardsByDistrict("760"); // phường xã của Quận 1 cũ
store.GetLegacyWardsByProvince("79"); // toàn bộ phường xã cũ của một tỉnh
LegacyDistrict có ProvinceCode, LegacyWard có DistrictCode. GetLegacyWardsByProvince bắc cầu qua quận/huyện — cần khi nhận diện địa chỉ cũ mà người dùng không ghi quận/huyện.
⚠️ 5 huyện đảo không có đơn vị cấp xã: Bạch Long Vĩ, Cồn Cỏ, Hoàng Sa, Lý Sơn, Côn Đảo.
GetLegacyWardsByDistricttrả về danh sách rỗng, không ném lỗi.
Không có vùng địa lý (Đông Bắc Bộ, Tây Nguyên...). Nguồn chính thức không công bố dữ liệu này — trường
Vùngtrong API tồn tại nhưng rỗng ở toàn bộ 34 tỉnh. Thư viện không bịa ra thứ nguồn không có.Vùng cũng đã bị bỏ khỏi mô hình mới là có lý do: sau sáp nhập, một tỉnh mới có thể trải trên nhiều vùng cũ. Phú Thọ mới = Vĩnh Phúc + Hoà Bình + Phú Thọ, ba tỉnh vốn thuộc các vùng khác nhau — gán vùng cho tỉnh mới không còn chính xác nữa.
Tìm kiếm không dấu
using VietnamAddress.Search;
store.SearchProvinces("Hồ Chí Minh"); // có dấu
store.SearchProvinces("ho chi minh"); // không dấu
store.SearchProvinces("hcm"); // viết tắt
store.SearchProvinces("TP.HCM"); // viết tắt kèm dấu chấm
store.SearchProvinces("hanoi"); // gõ liền, không dấu cách
store.SearchProvinces("79"); // theo mã
store.SearchWards("Bến Thành", provinceCode: "79"); // luôn giới hạn theo tỉnh
Trả về SearchHit<T>(Item, Score), sắp xếp theo điểm giảm dần:
| Điểm | Điều kiện |
|---|---|
| 1000 | Khớp mã đơn vị |
| 900 | Khớp chính xác tên (không dấu) |
| 850 | Khớp chính xác tên khi bỏ hết dấu cách (saigon → Sài Gòn) |
| 700 | Khớp tiền tố |
| 650 | Khớp tiền tố khi bỏ hết dấu cách |
| 500 | Chứa từ khoá |
| 300 | Khớp mọi từ, đảo thứ tự (nghe ben → Bến Nghé) |
Tầng "bỏ dấu cách" luôn xếp dưới tầng tương ứng có dấu cách, nên tên khớp đúng nguyên văn vẫn được ưu tiên hơn.
Cũng hiểu viết tắt người dùng hay gõ: p. = phường, x. = xã, q. = quận, tp. = thành phố.
Viết tắt tên tỉnh (hcm, tphcm, sai gon → Hồ Chí Minh; hn, dn, ct, hp) chỉ áp dụng khi tìm tỉnh, không áp dụng khi tìm phường xã. Lý do: từ 01/07/2025 "Sài Gòn" đã là tên một phường có thật, nên SearchWards("Sài Gòn", "79") phải trả về phường Sài Gòn chứ không phải bị viết lại thành "Hồ Chí Minh".
Chính tả i / y
Lý Sơn hay Lí Sơn, Mỹ Tho hay Mĩ Tho, Quy Nhơn hay Qui Nhơn — người dùng gõ kiểu nào cũng ra:
store.SearchWards("Li Son"); // -> Đặc khu Lý Sơn (900 điểm)
store.SearchWards("Qui Nhon"); // -> Phường Quy Nhơn (900 điểm)
Quy tắc này chỉ áp dụng khi so khớp (VietnameseText.SearchKey), không bao giờ đụng tới dữ liệu lưu trữ. Tên đơn vị hành chính là danh từ riêng — Ward.Name vẫn luôn là "Lý Sơn", không phải "Lí Sơn".
Và chỉ đổi khi y là nguyên âm duy nhất sau phụ âm đầu (Điều 9 quy định chính tả). Đổi bừa sẽ biến Tây Ninh thành Tai Ninh — nên tay, ngay, thuy giữ nguyên.
Quy đổi cũ ⇄ mới sau sáp nhập
Dựng từ bảng chuyển đổi ĐVHC chính thức: ánh xạ thẳng mã cũ ↔ mã mới, không suy diễn từ tên.
var merges = WardMergeMap.Instance;
merges.ToNewWards(legacyWardCode); // phường cũ -> WardMerge(Ward, Kind)
merges.ToLegacyWards(wardCode); // phường mới -> LegacyWardMerge[] (NHIỀU ứng viên)
merges.ToNewProvince("94"); // Sóc Trăng -> Cần Thơ
merges.ToLegacyProvinces("92"); // Cần Thơ mới <- Cần Thơ + Sóc Trăng + Hậu Giang
Mỗi kết quả kèm MergeKind — thứ quyết định kết quả có tin được không:
MergeKind |
Ý nghĩa | Số liên kết |
|---|---|---|
Full |
Nhập toàn bộ đơn vị cũ vào một đơn vị mới | 9.469 |
Partial |
Đơn vị cũ bị xé vào nhiều đơn vị mới | 969 |
Unchanged |
Giữ nguyên qua đợt sáp nhập | 127 |
foreach (var m in merges.ToNewWards(legacyCode))
{
if (m.Kind == MergeKind.Partial)
// Phường cũ bị xé -> chỉ tên là KHÔNG đủ, cần số nhà/đường/toạ độ.
Console.WriteLine($"{m.Ward.FullName} (một phần)");
}
Hai chiều KHÔNG đối xứng
Đây là bản chất bài toán, không phải thiếu dữ liệu.
Cũ → mới: dùng được. Trừ 441 phường xã cũ bị xé vào nhiều phường mới — với chúng, chỉ tên phường là không đủ, cần thêm số nhà/đường hoặc toạ độ. Kiểm bằng MergeKind.Partial.
Mới → cũ: vốn mơ hồ. Trung bình 3,2 phường cũ gộp thành 1 phường mới (cao nhất là 14). Biết phường mới thì không thể suy ngược ra chính xác phường cũ — thông tin đã mất khi gộp. Chỉ trả về danh sách ứng viên.
Phường Sài Gòn ← Bến Nghé + Đa Kao + Nguyễn Thái Bình
Phường Đức Nhuận ← Phường 4 + Phường 5 + Phường 9 (Quận Phú Nhuận cũ)
Độ phủ
| Phường xã cũ ánh xạ được | 10.033 / 10.035 (99,98%) |
| Phường xã mới truy được nguồn gốc | 3.316 / 3.321 (99,85%) |
| Tỉnh cũ được phủ | 63 / 63 |
Vì dựng từ bảng chuyển đổi chính thức (ánh xạ mã ↔ mã, không suy từ tên), độ phủ gần như tuyệt đối. Hai chỗ hụt:
- 5 phường mới không truy được nguồn gốc cấp xã là 5 đặc khu đảo (Bạch Long Vĩ, Cồn Cỏ, Hoàng Sa, Lý Sơn, Côn Đảo). Chúng vốn là huyện đảo không có đơn vị cấp xã, chuyển thẳng huyện → đặc khu. Không tồn tại ánh xạ xã→xã, nên rỗng ở đây là đúng, không phải thiếu.
- 2 xã cũ (
01651Mỹ Hưng,02482Phú Thịnh — Cao Bằng) bị sót trong bảng chính thức. Trả về rỗng chứ không đoán bừa.
Tách chuỗi địa chỉ tự do
using VietnamAddress.Parser;
var parser = new AddressParser(VietnamAddressStore.Instance, WardMergeMap.Instance);
var r = parser.Parse(diaChiNguoiDungGoTay);
| Thuộc tính | Ý nghĩa |
|---|---|
Detail |
Phần không thuộc danh mục hành chính: tên toà nhà, số nhà, tên đường. Giữ nguyên |
Province |
Tỉnh/thành theo địa giới mới |
Ward |
Phường/xã theo địa giới mới. null nếu không chốt được duy nhất |
WardCandidates |
Nhiều phường mới khả dĩ, khi phường cũ trong chuỗi bị xé ra nhiều nơi |
LegacyWard |
Phường/xã cũ nhận ra trong chuỗi (khi người dùng nhập địa chỉ cũ) |
LegacyWardCandidates |
Các phường xã cũ ứng với Ward |
Warnings |
Mâu thuẫn / điểm cần lưu ý trong chuỗi gốc |
IsResolved |
Có nhận ra được tỉnh không |
IsLegacyAddress |
Chuỗi gốc là địa chỉ theo địa giới cũ |
Đọc được địa chỉ CŨ — use case số một
Địa chỉ cũ đang nằm đầy trong database của bạn. Parser nhận ra và tự quy đổi sang đơn vị mới:
parser.Parse("Số 1 Đại Cồ Việt, Phường Bách Khoa, Quận Hai Bà Trưng, Hà Nội");
Detail : Số 1 Đại Cồ Việt
LegacyWard : Phường Bách Khoa ← không còn tồn tại
Ward : Phường Bạch Mai ← đơn vị mới tương ứng
Province : Thành phố Hà Nội
⚠️ 'Quận Hai Bà Trưng' là cấp quận/huyện — đã bãi bỏ từ 01/07/2025.
Kể cả khi tỉnh đã bị xoá sổ:
parser.Parse("12 Trần Hưng Đạo, Phường 3, Thành phố Sóc Trăng, Tỉnh Sóc Trăng");
LegacyWard : Phường 3
Ward : Phường Phú Lợi
Province : Thành phố Cần Thơ ← Sóc Trăng đã nhập vào Cần Thơ
⚠️ 'Tỉnh Sóc Trăng' là địa giới cũ — nay thuộc Thành phố Cần Thơ.
Khi phường cũ bị xé — Ward là null, KHÔNG đoán bừa
var r = parser.Parse(diaChiCu);
if (r.Ward is not null) // chốt được duy nhất
Luu(r.Ward.Code);
else if (r.WardCandidates.Count > 0)
// Phường cũ nằm trên nhiều phường mới -> phải hỏi lại người dùng,
// hoặc dùng số nhà/toạ độ mới phân giải được.
HoiLai(r.WardCandidates);
Chỉ khớp CHÍNH XÁC
Parser không khớp gần đúng. Xã Ea Tu sẽ không bao giờ âm thầm nhận thành Xã Ea Tul — thà trả null còn hơn trả sai. Đây từng là bug thật, và là loại tệ nhất: sai mà không báo lỗi.
Không có dấu phẩy, hoặc viết kiểu tiếng Anh — vẫn đọc được
parser.Parse("5 Lê Lợi Phường Bến Nghé Quận 1 TP.HCM"); // không một dấu phẩy nào
parser.Parse("5 Le Loi, Ben Nghe Ward, District 1, Ho Chi Minh City");
Không có dấu phẩy thì chuỗi được cắt trước mỗi từ khoá loại đơn vị (Phường, Quận, TP…). Có dấu phẩy thì tôn trọng cách người dùng đã phân đoạn, không cắt lại — cắt lại sẽ xé nhầm những tên chứa chính từ khoá đó (Phường Phương Liệt → Phường Phương + Liệt).
Tiếng Việt viết loại đơn vị ở trước (Phường Bến Nghé), tiếng Anh viết ở sau (Ben Nghe Ward) — cả hai đều nhận.
Quận/huyện cũ dùng để KHOANH VÙNG, không phải để vứt
Cấp quận/huyện đã bãi bỏ, nhưng trong chuỗi địa chỉ cũ nó là thứ duy nhất phân biệt được phường trùng tên. Phường 1 tồn tại ở 11 quận/huyện cũ chỉ riêng trong phạm vi TP.HCM mới (vốn gộp từ Bình Dương + Bà Rịa-Vũng Tàu + Hồ Chí Minh). Không có nó, "Phường 10, Gò Vấp" có thể khớp nhầm sang Phường 10 của Vũng Tàu.
Thiếu quận/huyện mà tên phường trùng nhiều nơi → parser trả null kèm cảnh báo liệt kê các quận xung đột, không đoán.
Những gì parser KHÔNG làm
Số nhà, tên đường, tên toà nhà không nằm trong bất kỳ dataset hành chính nào — parser giữ nguyên chúng ở Detail. Đây cũng là cách các sàn TMĐT Việt Nam làm: dropdown Tỉnh → Phường/Xã, rồi một ô text riêng cho phần còn lại.
Không có dữ liệu địa lý — không toạ độ, không ranh giới, không polygon. Từ (lat, lng) không tra ra được phường. Muốn vậy phải có polygon ranh giới và point-in-polygon (PostGIS chẳng hạn); thư viện này chỉ là danh mục mã + tên.
Ghép chuỗi địa chỉ
using VietnamAddress.Format;
var r = parser.Parse("22 Hẻm 141/22 Đường Số 339, Phường Phước Long B, Thành phố Thủ Đức, TP. Hồ Chí Minh");
r.ToNewAddress(); // 22 Hẻm 141/22 Đường Số 339, Phường Phước Long, Thành phố Hồ Chí Minh
r.ToLegacyAddress(); // 22 Hẻm 141/22 Đường Số 339, Phường Phước Long B, Thành phố Thủ Đức, Thành phố Hồ Chí Minh
r.ToNewAddress(AddressStyle.Short); // ..., Phước Long, Hồ Chí Minh
r.ToNewAddress(AddressStyle.English); // ..., Phuoc Long Ward, Ho Chi Minh City
Hiển thị song song hai dạng cho người quen tên cũ vẫn đọc được. ToLegacyAddress() trả rỗng nếu chuỗi gốc vốn không phải địa chỉ cũ — biết phường mới thì không suy ngược ra được phường cũ duy nhất.
Toạ độ tâm
Tâm và diện tích của 34 tỉnh + 3.321 phường/xã (địa giới mới), nhúng sẵn — thêm 46 KB.
using VietnamAddress.Geo;
var ward = store.GetWardsByProvince("79").First(w => w.Name == "Bến Thành");
ward.Centroid(); // Centroid { Center = (10.77072, 106.69452), AreaKm2 = 1.85 }
province.Centroid();
new Coordinate(21.0287, 105.8524)
.DistanceKm(new Coordinate(10.7725, 106.6980)); // 1140 km, Hà Nội -> TP.HCM
store.NearestWard(new Coordinate(10.7725, 106.698), "79"); // -> Bến Thành
⚠️ NearestWard KHÔNG phải "phường chứa toạ độ này"
Đo thật trên 10 điểm rải rác toàn quốc, đối chiếu với ranh giới thật: 3 điểm cho kết quả sai.
| Toạ độ | Ranh giới thật | NearestWard |
|---|---|---|
| Sân bay Tân Sơn Nhất | Hạnh Thông | Tân Sơn ✗ |
| Suối Tiên | Linh Xuân | Đông Hòa ✗ |
| Đà Lạt | Xuân Hương - Đà Lạt | Lâm Viên - Đà Lạt ✗ |
Đây là bản chất, không phải lỗi: "gần tâm nhất" và "nằm bên trong" là hai câu hỏi khác nhau. Đứng ở rìa một phường rộng thì tâm phường bên cạnh hoàn toàn có thể gần bạn hơn.
Dùng nó để gợi ý khi người dùng thả ghim (rồi để họ xác nhận), sắp xếp theo độ gần, ước lượng nhanh. Đừng dùng để chốt địa chỉ chính thức — sai ~30% mà không báo lỗi.
Muốn biết chính xác một toạ độ nằm trong phường nào thì phải có polygon ranh giới và làm point-in-polygon (PostGIS chẳng hạn). Thư viện này không có dữ liệu đó.
Mặc định nó luôn trả về một phường, dù toạ độ ở Tokyo. Truyền maxDistanceKm để nó chịu trả null:
store.NearestWard(tokyo); // -> "Móng Cái 1" (!)
store.NearestWard(tokyo, maxDistanceKm: 50); // -> null
Không đặt ngưỡng mặc định vì không có con số nào an toàn cho mọi trường hợp: xã đất liền rộng nhất chỉ có bán kính ~19 km, nhưng Đặc khu Trường Sa là quần đảo rải rác hàng trăm km — tâm của nó nằm giữa biển. Ngưỡng cứng sẽ âm thầm làm hỏng tra cứu ở Trường Sa và Hoàng Sa. Với đất liền, 50 là hợp lý.
Không có toạ độ cho đơn vị CŨ
Centroid() của mọi đơn vị theo địa giới cũ đều trả null. Không tồn tại nguồn ranh giới cấp xã cũ vừa miễn phí, vừa có mã đơn vị hành chính, vừa cho dùng thương mại — các nguồn hiện có chỉ có tên, mà ghép theo tên là cầm chắc gán sai (xem mục dưới).
Tâm được tính offline, đối chiếu mã với danh mục chính thức trước khi nhúng — lệch một mã là dừng, không ghi file.
⚠️ Mã phường KHÔNG duy nhất xuyên thời gian
Đọc kỹ mục này trước khi thiết kế bảng trong database.
1.846 mã phường trỏ tới hai đơn vị khác nhau tuỳ thời kỳ:
| Mã | Nghĩa MỚI (từ 01/07/2025) | Nghĩa CŨ |
|---|---|---|
00070 |
Hoàn Kiếm | Hàng Trống |
00004 |
Ba Đình | Trúc Bạch |
00082 |
Cửa Nam | Trần Hưng Đạo |
Phường mới thường kế thừa mã của một trong các phường cũ hợp thành nó. Nên nếu DB lưu ward_code = 00070 từ trước 01/07/2025 (ý là Hàng Trống) rồi tra bảng mới, bạn nhận về Hoàn Kiếm — một phường khác hẳn, rộng gấp nhiều lần. Sai mà không báo lỗi.
Đừng lưu một mã trần. Lưu cả ward_code (mới) lẫn legacy_ward_code (cũ), hoặc kèm cờ thời kỳ.
Công cụ dòng lệnh — quy đổi hàng loạt
Database của bạn đang đầy địa chỉ cũ. tools/VietnamAddress.Cli quy đổi cả file một lượt:
dotnet run --project tools/VietnamAddress.Cli -- \
migrate hocsinh.csv --column dia_chi -o hocsinh.migrated.csv
OK : 4821 (96.4%) nhập thẳng được
AMBIGUOUS : 152 (3.0%) CẦN NGƯỜI XỬ LÝ
UNRESOLVED : 27 (0.6%) không nhận ra
File ra có đủ ward_code / legacy_ward_code / legacy_district / warnings, và cột status chia ba:
status |
Ý nghĩa |
|---|---|
OK |
Chốt được phường mới — nhập thẳng vào DB |
AMBIGUOUS |
Phường cũ bị xé, hoặc chuỗi thiếu quận/huyện — phải có người quyết |
UNRESOLVED |
Không nhận ra |
Lọc status=AMBIGUOUS rồi xử lý tay. Đừng lấy đại ứng viên đầu tiên — sai phường là sai địa chỉ, và không ai phát hiện ra.
Cũng tra được một địa chỉ lẻ:
dotnet run --project tools/VietnamAddress.Cli -- parse "22 Đường 339, Phường Phước Long B, Thủ Đức, TP.HCM"
Đóng góp
Tìm thấy lỗi dữ liệu? Tạo Issue kèm thông tin cụ thể, hoặc gửi Pull Request.
Script sinh dữ liệu nằm trong tools/ — khi có nghị quyết mới, chạy lại là ra bộ dữ liệu mới.
Giấy phép
MIT. Dùng thoải mái, kể cả thương mại. Cung cấp theo nguyên trạng ("as is"), không kèm bảo đảm nào.
Dữ liệu đơn vị hành chính là dữ liệu công do nhà nước công bố. Thư viện chỉ kéo về, chuẩn hoá và đóng gói.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 179 | 7/15/2026 |