resolve_parcel
Converts a Korean address or PNU into canonical parcel data: PNU, land category, refined address, WGS84 coordinates, official land price, and administrative hierarchy.
Instructions
Resolve address or PNU → canonical parcel: PNU, 지번, parsed 지목 (e.g. 공장용지), refined address, WGS84 coordinates, 공시지가, administrative hierarchy. Foundation for all other tools.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Korean address (e.g. '경기도 평택시 포승읍 내기리 680') or 19-digit PNU |
Implementation Reference
- src/tools/resolve_parcel.ts:5-29 (handler)The tool handler function. Calls resolveToPoint to resolve address/PNU, parses jibun for 지목 info, and returns a structured result with PNU, address, coordinates, land price, administrative info, and disclaimer.
export const resolveParcelTool = async ({ query }: { query: string }) => { const r = await resolveToPoint(query); const parsed = parseJibun(r.jibun); const result = { pnu: r.pnu, jibun: r.jibun, jimok_code: parsed.jimok_code, jimok: parsed.jimok_full_name, is_mountain: parsed.is_mountain, address: r.address, point_wgs84: r.point_wgs84, land_price_won_per_m2: r.land_price_won_per_m2, land_price_gosi_date: r.land_price_gosi_date, administrative: r.administrative, source: { geocoder_layer: r.geocoder_layer, parcel_layer: "vworld LP_PA_CBND_BUBUN", resolved_at: new Date().toISOString(), }, disclaimer: DISCLAIMER, }; return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], }; }; - src/server.ts:33-38 (registration)Registration of the 'resolve_parcel' tool on the MCP server with description and zod schema for the query parameter.
server.tool( "resolve_parcel", "Resolve address or PNU → canonical parcel: PNU, 지번, parsed 지목 (e.g. 공장용지), refined address, WGS84 coordinates, 공시지가, administrative hierarchy. Foundation for all other tools.", QUERY_SCHEMA, resolveParcelTool ); - src/server.ts:26-31 (schema)Zod schema for the tool's input: a 'query' string of min length 2 (Korean address or 19-digit PNU).
const QUERY_SCHEMA = { query: z .string() .min(2) .describe("Korean address (e.g. '경기도 평택시 포승읍 내기리 680') or 19-digit PNU"), }; - src/lib/resolve.ts:49-163 (helper)Core resolution logic: resolveParcelCore handles PNU lookup (via V-World LP_PA_CBND_BUBUN layer) and address geocoding (via V-World geocoder), returning parcel properties, point, and land price.
async function resolveParcelCore( query: string, includeParcelGeometry: boolean ): Promise<ResolvedPoint & { _parcel_geometry?: unknown }> { const trimmed = query.trim(); let point: { x: number; y: number }; let geocoderLayer: string; let admin: ResolvedPoint["administrative"] = {}; if (isPnu(trimmed)) { const feats = await getFeatures({ layer: "LP_PA_CBND_BUBUN", attrFilter: `pnu:=:${trimmed}`, includeGeometry: true, size: 1, }); if (feats.length === 0) { throw new VWorldError(`PNU ${trimmed} not found in LP_PA_CBND_BUBUN`, "PNU_NOT_FOUND"); } const centroid = centroidFromGeometry(feats[0].geometry); if (!centroid) { throw new VWorldError(`PNU ${trimmed}: could not derive centroid`, "COORD_PARSE_FAILED"); } point = centroid; geocoderLayer = "LP_PA_CBND_BUBUN (attrFilter pnu, centroid)"; const p = feats[0].properties; const jiga = toNumberOrNull(p.jiga); const gosiYear = p.gosi_year ? String(p.gosi_year) : null; const gosiMonth = p.gosi_month ? String(p.gosi_month) : null; const gosiDate = gosiYear && gosiMonth ? `${gosiYear}-${gosiMonth.padStart(2, "0")}` : null; return { pnu: p.pnu ? String(p.pnu) : null, jibun: p.jibun ? String(p.jibun) : null, address: p.addr ? String(p.addr) : null, point_wgs84: point, land_price_won_per_m2: jiga, land_price_gosi_date: gosiDate, administrative: admin, geocoder_layer: geocoderLayer, _parcel_geometry: feats[0].geometry, }; } let geo; try { geo = await geocodeAddress(trimmed, "parcel"); } catch (e) { if (e instanceof VWorldError) geo = await geocodeAddress(trimmed, "road"); else throw e; } point = geo.point_wgs84; geocoderLayer = "vworld geocoder /req/address"; admin = { sido: geo.structure.level1, sigg: geo.structure.level2, emd_dong: geo.structure.level4L || geo.structure.level4A, emd_dong_code: geo.structure.level4LC || geo.structure.level4AC, }; const parcelFeats = await getFeatures({ layer: "LP_PA_CBND_BUBUN", point, size: 1, includeGeometry: includeParcelGeometry, }); if (parcelFeats.length === 0) { return { pnu: null, jibun: null, address: null, point_wgs84: point, land_price_won_per_m2: null, land_price_gosi_date: null, administrative: admin, geocoder_layer: geocoderLayer, _parcel_geometry: undefined, }; } const p = parcelFeats[0].properties; const jiga = toNumberOrNull(p.jiga); const gosiYear = p.gosi_year ? String(p.gosi_year) : null; const gosiMonth = p.gosi_month ? String(p.gosi_month) : null; const gosiDate = gosiYear && gosiMonth ? `${gosiYear}-${gosiMonth.padStart(2, "0")}` : null; return { pnu: p.pnu ? String(p.pnu) : null, jibun: p.jibun ? String(p.jibun) : null, address: p.addr ? String(p.addr) : null, point_wgs84: point, land_price_won_per_m2: jiga, land_price_gosi_date: gosiDate, administrative: admin, geocoder_layer: geocoderLayer, _parcel_geometry: includeParcelGeometry ? parcelFeats[0].geometry : undefined, }; } export async function resolveToPoint(query: string): Promise<ResolvedPoint> { const r = await resolveParcelCore(query, false); const { _parcel_geometry, ...rest } = r; void _parcel_geometry; return rest; } export async function resolveToParcel(query: string): Promise<ResolvedParcel> { const r = await resolveParcelCore(query, true); const { _parcel_geometry, ...rest } = r; const wkt = geometryToWKT(_parcel_geometry); return { ...rest, parcel_geometry: _parcel_geometry ?? null, parcel_wkt: wkt }; } - src/lib/jimok.ts:40-50 (helper)Parses a jibun (지번) string to extract mountain flag, number part, and 지목 code/name from a 28-type mapping table.
export function parseJibun(raw: string | null | undefined): ParsedJibun { const src = String(raw ?? "").trim(); const is_mountain = /^산\s?\d/.test(src); const m = src.match(/(\d+(?:[-\s]\d+)?)\s*([가-힣])?\s*$/); const number_part = m?.[1] ?? null; const jimok_code = m?.[2] ?? null; const jimok_full_name = jimok_code ? JIMOK_CODE_MAP[jimok_code] ?? null : null; return { raw: src, is_mountain, number_part, jimok_code, jimok_full_name }; }