VietnamAddress 1.0.0

dotnet add package VietnamAddress --version 1.0.0
                    
NuGet\Install-Package VietnamAddress -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="VietnamAddress" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="VietnamAddress" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="VietnamAddress" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add VietnamAddress --version 1.0.0
                    
#r "nuget: VietnamAddress, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package VietnamAddress@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=VietnamAddress&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=VietnamAddress&version=1.0.0
                    
Install as a Cake Tool

<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" />

NuGet Downloads License: MIT .NET

</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 provinceCode khi 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. GetLegacyWardsByDistrict trả 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ùng trong 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ũ (01651 Mỹ Hưng, 02482 Phú 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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