smartParsePro V4: offline Chinese address parsing that returns ranked candidates
🔥地址智能识别Pro(支持省市区街道/电话/邮编/姓名识别)
At a glance
- What is it?
- smartParsePro is a JavaScript library for splitting a Chinese address string into province, city, county, street, detail, name, phone, postal code and ID card, running entirely offline with no runtime dependencies. Its distinguishing design choice in V4 is that parseAddress always returns an array of ranked candidates and never picks the first one for you.
- Who is it for?
- Adopt smartParsePro V4 if you need to read free-form Chinese address text on a server or in a browser without shipping the addresses anywhere, since the parser runs offline, has no runtime dependencies, and returns the province, city, county, street, name, phone and postal code fields separately.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 86 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why parseAddress returns an array instead of one answer
The core design decision in V4 is stated in the README in one line: `parseAddress()` always returns an array of candidates and never selects the first one by default.
The reason is that Chinese administrative divisions contain a large number of identically named counties and townships. The README's example is `鼓楼区中山北路1号`, a Gulou district with a Zhongshan North Road, which could be Nanjing, Xuzhou, Fuzhou or Kaifeng. V4 returns every valid path that clears the quality threshold.
const results = parseAddress('鼓楼区中山北路1号')
console.log(results.length) // 4The candidates are sorted by score, but the README is careful about what `rank: 1` means. It only indicates the highest scoring candidate at that moment, it does not mean the first result has been confirmed, and there is no `selected` field in the result. The caller is expected to disambiguate using the user's location, the postal code, historical orders or a manual choice.
A single unambiguous input still returns a one element array, and an input with no administrative division match returns an empty array. So the return type never changes shape, which is the point: a caller written against V4 does not need a branch for the confident case and a branch for the ambiguous case.
What the parser pulls out of a single line of text
The result object is deliberately verbose, and the verbosity is where the honesty lives. Each candidate carries a `candidateId` generated from the administrative path, a `rank` starting at 1, the `detail` string left after the divisions and recognised entities are removed, and separate `name`, `phone`, `postalCode` and `idCard` fields.
The identity card field is worth noting because it is validated. The README describes `idCard` as an identity card that passed the check-digit verification, so a malformed number does not silently appear in the output. Phone numbers are normalised, and `postalCode` is the six digit code.
The `scoreReasons` array explains the ranking in machine readable form, with values such as `FULL_PROVINCE_MATCH`, `FULL_COUNTY_MATCH`, `ORDERED_LEVELS`, `ADJACENT_LEVELS` and `STRONG_HIERARCHY`, and `warnings` carries the caveats, including `AMBIGUOUS_ADDRESS` for multiple valid administrative paths, `CANDIDATES_TRUNCATED` when the count exceeded `maxCandidates`, `INFERRED_ADMIN_LEVELS` where a level was inferred rather than read from the text, `LOW_CONFIDENCE` for weak evidence and `MULTIPLE_PHONES` when more than one number was found.
The README is explicit that `confidence` is a rule based score used for ranking and filtering, and that it should not be treated as a statistically meaningful probability. That is a small sentence, and it is the difference between a useful parser and a misleading one.
Distinguishing a matched level from an inferred one
Each administrative level is returned as a `DivisionResult`, and the interface is small.
interface DivisionResult {
code: string
name: string
sourceName: string
confidence: number
matched: boolean
inferred: boolean
matchedText?: string
}Two of those fields carry most of the meaning. `sourceName` keeps the original name as it appears in the administrative division data, which can differ from the normalised `name`. And `matched: false` with `inferred: true` means the level was derived from the parent and child administrative path rather than appearing literally in the input text.
Municipalities are where this becomes visible. Beijing, Tianjin, Shanghai and Chongqing keep a full province, city and county structure in the business output, so a Beijing address still has three levels even though the city level is an artefact. The README's example shows it directly.
const [result] = parseAddress('北京市东城区东华门街道111号')
result.province.name // 北京市
result.city.name // 北京市
result.city.sourceName // 市辖区
result.city.matched // false
result.city.inferred // trueThe parser does not modify the administrative data the caller passes in, and a single occurrence of a Beijing style text span counts as explicit evidence for one administrative level only, so the city level comes out as inferred. Without the `inferred` flag, a caller would have to guess whether the duplicated 北京市 was really two levels of evidence or one.
Installing the SDK and reading a candidate list
V4 is published to npm, and the README states the environment requirements: Node.js 22 or newer, modern browsers, the parsing core running offline so addresses are never uploaded, and no third-party runtime dependencies in the current implementation.
npm install address-smart-parse@^4Two packages are published. The SDK is `address-smart-parse` and the HTTP API server is `address-smart-parse-server`.
The import style depends on the module system. This is the ESM form, which reads each level of a candidate.
import { parseAddress } from 'address-smart-parse'
const results = parseAddress(
'陕西省西安市雁塔区丈八沟街道高新四路高新大都荟710061 刘国良 13593464918'
)
for (const candidate of results) {
console.log(candidate.province?.name)
console.log(candidate.city?.name)
console.log(candidate.county?.name)
console.log(candidate.street?.name)
console.log(candidate.detail)
}The CommonJS form is the same call with a different import.
const { parseAddress } = require('address-smart-parse')
const results = parseAddress(
'广东省珠海市香洲区盘山路28号幸福茶庄,陈景勇,13593464918'
)
console.dir(results, { depth: null })The optional chaining on `candidate.province?.name` is not decoration. Since the result is an array of candidates and a level may have been inferred, defensive access is the correct style, and a caller that assumed every level was present would break on the empty array case.
Tuning thresholds, and what maxCandidates does not mean
The parse options are passed as a second argument and are few.
const results = parseAddress(input, {
minConfidence: 0.3,
maxCandidates: 10,
extractIdCard: false
})`minConfidence` defaults to `0.18` and filters out candidates scoring below that, `maxCandidates` defaults to `20` and caps the number returned, and `extractIdCard` defaults to `true`, meaning the identity card is included in the output and can be switched off.
The README makes one point sharply about `maxCandidates`. Even when you set it to 1, V4 will not claim the result is confirmed. It only limits how many are returned. That is a deliberate refusal to let a tuning knob double as a confidence signal, and it matters for anyone building a checkout flow on top: setting `maxCandidates: 1` and treating the single candidate as the address is exactly the mistake the V4 design exists to prevent.
The warning `CANDIDATES_TRUNCATED` is the visible trace of that limit. When it appears, the array is not the complete set of plausible parses, and a caller that wanted a full ambiguity picture should raise the cap rather than treat the truncation as an absence of alternatives.
Custom division data, and why createParser exists
The parser accepts custom administrative division data, in a format consistent with the common `streets.json` province, city, county and street tree, with a `code`, a `name` and a `children` array at each level.
The V3 style call passes that data as the second argument, and the third argument still takes the parse options.
import { parseAddress } from 'address-smart-parse'
import customAddressData from './custom-streets.json'
const results = parseAddress(
'测试省示例市未来区星河街道创新路1号',
customAddressData
)The second argument is interpreted by type: an array is treated as custom division data, a plain object is treated as parse options. The README notes the cost of this style, that the custom index is rebuilt on every call, and that it suits occasional calls only.
For high frequency or batch work the recommended approach is `createParser()`, which builds the index once and reuses it.
import { createParser } from 'address-smart-parse'
import customAddressData from './custom-streets.json'
const parser = createParser({
divisions: customAddressData,
dataVersion: 'company-2026-07',
aliases: {
"990000": ["测试", "测试地区"]
}
})
const first = parser.parseAddress(address1)
const second = parser.parseAddress(address2)The `dataVersion` field is worth noticing because it identifies which dataset produced a result. The `aliases` map attaches extra surface forms to a division code, so an address written with a nickname or an abbreviation still resolves. For a company with its own internal place names, that map is the mechanism for making the parser understand the words its users actually type.
The repository, the scripts and the V3 migration
The npm package is one part of the project, and the repository itself is a workspace with two members, `packages/sdk` and `packages/server`, under the private root package `smart-parse-pro-v4-workspace` at version `4.0.3`.
The scripts describe the full pipeline. `build` runs `node scripts/build.mjs`, `test` runs `node --test tests/*.test.cjs`, and `test:v4` chains the build and the test together, which is the command the README gives for a complete build and test pass. There is also `generate:corpus` and `evaluate:corpus` for building and scoring a test corpus, a `start:api` script for the HTTP server, and `start:forever` for the longer running variant.
The engine requirement is `>=22`, matching the README's stated environment. Working from a clone rather than npm looks like this.
git clone https://github.com/wzc570738205/smartParsePro.git
cd smartParsePro
npm install
npm run build
npm testThe repository uses npm workspaces, so after a single `npm install` the package name resolves locally and the same import works from inside the tree.
const { parseAddress } = require('address-smart-parse')Two sample programs are checked in, `examples/v4.cjs` and `examples/v4.mjs`, run with `node examples/v4.cjs` and `node examples/v4.mjs`, along with a `examples/browser.html` for the browser build. There is also a `docs/` directory and a `tests/` directory at the top level.
Migration from the previous version is supported through a V3 `smart()` entry point, and the README points old users at the V3 source and documentation on the `3.1` tag, which is fixed on the remote, while the current branch and documentation are V4. One caveat is worth stating: the README describes the `name` and `phone` and `postalCode` fields on the candidate, and the `idCard` field depends on the `extractIdCard` option, so a V3 caller that expected a single object will need to change both the call site and the handling of multiple candidates.
Editorial conclusion
Adopt smartParsePro V4 if you need to read free-form Chinese address text on a server or in a browser without shipping the addresses anywhere, since the parser runs offline, has no runtime dependencies, and returns the province, city, county, street, name, phone and postal code fields separately. Do not adopt it if your input is already structured, because the whole point of the library is to untangle messy text, and do not read the returned `confidence` as a probability, since the README states it is a rule-based score used for ranking and filtering. Before you ship it, decide how you will disambiguate multiple candidates, because V4 deliberately hands that back to the caller, and check that the administrative division data you pass in covers the regions your users write addresses from.
Frequently asked questions
What does smartParsePro V4 return for an ambiguous Chinese address?
It returns an array of all candidates that clear the quality threshold, sorted by score. The README's example, `鼓楼区中山北路1号`, returns 4 candidates because that district and road name exist in several cities, and `rank: 1` only marks the highest scoring one without confirming it.
How do I install smartParsePro and call parseAddress?
Run `npm install address-smart-parse@^4` and import the function, either with `import { parseAddress } from 'address-smart-parse'` or with `require('address-smart-parse')`. The library requires Node.js 22 or newer, a modern browser, and has no third-party runtime dependencies.
Does smartParsePro upload the addresses it parses?
No. The README states that the parsing core runs offline and does not upload addresses, and that the current implementation has no third-party runtime dependencies.
What is the difference between matched and inferred in the division results?
A level with `matched: false` and `inferred: true` was derived from the parent and child administrative path rather than appearing literally in the input text. The README uses the Beijing example, where the city level comes back as `市辖区` with `inferred` set to true, because a single occurrence of a Beijing style text span is evidence for one administrative level only.
How do I supply my own administrative division data?
Pass it as the second argument to `parseAddress`, in the usual `streets.json` province, city, county and street tree shape, with the parse options in the third argument. For repeated calls the README recommends `createParser()`, which builds the custom index once, accepts a `dataVersion` label and an `aliases` map, and exposes `parser.parseAddress`.
What do the warnings in the parse result mean?
The README lists five: `AMBIGUOUS_ADDRESS` for multiple valid administrative paths, `CANDIDATES_TRUNCATED` when the candidate count exceeded `maxCandidates`, `INFERRED_ADMIN_LEVELS` for levels taken from the path, `LOW_CONFIDENCE` for weak evidence, and `MULTIPLE_PHONES` when more than one phone number was found in the input.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/wzc570738205-smartparsepro)Was this useful?