> **本文回答**：`svc.*QueryFeature` 系列返回的坐标到底在哪个空间、`.geom`/`.geojson`/`.points`/`.envelop` 各是什么、怎么正确叠到图上。（与 [backend/query-features](../backend/query-features.md) 互补：那边讲接口用法，这里讲**坐标空间**。）

## 一、服务端返回什么——各字段坐标空间对照

查询接口（`pointQueryFeature`/`rectQueryFeature`/`conditionQueryFeature`/`exprQueryFeature`）返回 `res.result[]`。**以 sys_cad2000 为例，每个字段的坐标空间并不一样**：

| 字段 | 类型 | 坐标空间 | 何时有值 |
|---|---|---|---|
| `geojson` | string（GeoJSON） | **原始墨卡托**（默认）/ **CAD 坐标**（`toMapCoordinate:true`，或内存模式结果 `isgeocoord:true`） | 条件查询仅 `includegeom:true` 时才有值（只传 `realgeom` 无效；多条且无 `realgeom` 时为外包矩形多边形）；点/矩形查询总有值；指定 `fields` 时须包含 `geojson` |
| `geom` | object | **渲染经纬度**（SDK `_processQueryResult` 转换产物） | `geojson` 非空 **且** `isMapCoordinate!==true` 时才生成；同时附 `isline/isPoint/isPolygon`（⚠️ `isPolygon` 有 SDK bug，见 §三） |
| `envelop` | string WKT `POLYGON((...))` | **原始墨卡托**（值可为负的百万级，别当 CAD！） | 实体外包矩形，几乎总有；`map.getEnvelopBounds(envelop)` 解析为 CAD 空间 GeoBounds |
| `bounds` | string `"[x1,y1,x2,y2]"` | **CAD 坐标** | 外包矩形，几乎总有 |
| `points` | string `"x,y;x,y"` | **CAD 坐标** | 顶点串 |
| `location`/`center`/`insertionPoint`/`origin` | string `"x,y"` | **CAD 坐标** | 点状实体/文字 |
| `isMapCoordinate` | bool | — | `toMapCoordinate:true` 时服务端置 true |
| `isEnvelop` | bool | — | 该结果只给了外包矩形（`geojson` 为空）时多为 true |
| `isgeocoord` | bool | — | 响应顶层字段：内存模式查询置 true，表示 `geojson` 已是 CAD 坐标（非墨卡托） |

请求参数对返回的影响：

| 参数 | 效果 |
|---|---|
| `geom:true` | 几何查询模式；**不等于**返回 geojson |
| `includegeom:true` | 返回 `geojson`（多条时可能只是 envelop 几何） |
| `realgeom:true` | 每条都返回真实实体 geojson（配合 includegeom） |
| `toMapCoordinate:true` | `geojson` 变 CAD 坐标 + `isMapCoordinate:true`；`.geom` 不生成 |
| 查询落空 | **没有 `result` 数组**：条件查询为 `{recordCount:0}`，点/矩形查询多为 `{}`——`res.result ?? []` 兜底 |

## 二、SDK 侧 `_processQueryResult` 做了什么（Service.js）

```ts
const isgeocoord = param.isgeocoord;                    // 响应顶层字段，不是每条结果上的
const p = this.currentMapParam();
if (p) {
  prj = new GeoProjection(p.bounds);                    // 当前图范围投影
  if (!isgeocoord && p.mapBounds) mapPrj = new GeoProjection(p.mapBounds);  // 注意 !isgeocoord 才建
}
if (!isgeocoord && Array.isArray(param.metadata?.mapBounds))  // 必须是数组
  mapPrj = prj = new GeoProjection(GeoBounds.fromArray(param.metadata.mapBounds));
```

然后对每个 result 处理（cb 优先级最高，见 §五.8），伪代码：

```text
  if (!data[d].geojson) → continue（envelop 模式不产生 .geom）
  if (res.isMapCoordinate === true) → continue（不生成 .geom）
  else 解析 res.geojson 到新对象，逐坐标转换后写入 res.geom：
    isgeocoord  →  prj ? prj.toLngLat(cad) : 原样保留               // CAD → 渲染经纬度
    否则        →  (mapPrj && prj) ? prj.toLngLat(mapPrj.fromMercator(mkt))  // 墨卡托→CAD→渲染经纬度
                     : Projection.mercator2LngLat(mkt)               // 纯墨卡托→经纬度
  然后写类型标记：
    if (!isLine && res.isEnvelop) isLine = true;        // ⚠️ envelop 结果被强制标记成线
    result[d].isline = isLine; result[d].isPoint = isPoint;
    if (result[d].isline === false && result[d].isline === false) result[d].isPolygon = true;
    //  ↑ SDK 源码把 isline 判断了两次（本该判 isPoint），见 §三警告
```

> 注意：被逐点改写的是 `JSON.parse(res.geojson)` 出来的**临时对象**（赋给 `.geom`）；`.geojson` 字符串本身保持原始空间不变——两者坐标空间不同，别混用。
>
> 若 `svc` 从未 `openMap`（`currentMapParam()` 为空）：`prj`/`mapPrj` 均为 null——`isgeocoord` 结果（内存模式）**原样保留 CAD**，其余走 `mercator2LngLat` 得到**真实经纬度**（此时 `.geom` 就不是"渲染经纬度"了）。查询前确保 `map.attach(svc, prj)`/`svc.openMap` 已建立上下文。

**结论：`res.result[i].geom` 永远是"渲染经纬度"坐标**——CAD 底图下是 `GeoProjection` 归一化经纬度，互联网底图下是真实经纬度。可直接：

```js
map.getSource('x').setData({ type:'Feature', geometry: ent.geom.geometries[0] });
// 或点：
marker.setLngLat(ent.geom.geometries[0].coordinates);
```

而 `toMapCoordinate:true` 的结果**没有 `.geom`**，要自己 `JSON.parse(res.geojson)`，得到 **CAD 坐标**。

## 三、⚠️ `isPolygon` 不可信（SDK bug，`Service.js` 源码核实）

判定条件重复写了 `isline === false`（本意应是 `isPoint === false`），所以**纯点实体也会被打上 `isPolygon: true`**。另外 `isEnvelop` 的结果会被强制 `isline = true`。

判断几何类型请：先看 `isPoint`，再看 `isline`，或直接读 `geom.geometries[i].type`。

## 四、三种典型用法

### 4.1 只要位置点画 Marker（最省流量）

```js
const res = await svc.conditionQueryFeature({
  condition: "name='13' and text like '%泵房%'",
  fields: "objectid,points,location,text",
});
for (const e of res.result ?? []) {        // 落空时 result 可能不存在！
  const [x, y] = (e.location ?? e.points).split(';')[0].split(',').map(Number); // CAD
  new vjmap.Marker().setLngLat(map.toLngLat([x, y])).addTo(map);   // CAD→渲染经纬度
}
```

### 4.2 要真实几何高亮/GeoJSON 图层

```js
const res = await svc.conditionQueryFeature({
  condition: "name='11' and layerindex=3",
  includegeom: true, realgeom: true,   // 关键：不传只有 envelop，geojson 为空、.geom 不生成
});
const features = (res.result ?? []).filter(e => e.geom).map(e => ({
  type: 'Feature', properties: { objectid: e.objectid }, geometry: e.geom.geometries[0],
}));
map.addSource('hl', { type:'geojson', data:{ type:'FeatureCollection', features } }); // .geom 已是渲染经纬度
```

### 4.3 要 CAD 坐标做计算/转 mapext 实体

```js
const res = await svc.conditionQueryFeature({
  condition: "...",
  toMapCoordinate: true,               // 关键：geojson 直接给 CAD 地图坐标
  includegeom: true, realgeom: true,   // 同样必须，否则 geojson 是 ""
});
for (const e of res.result ?? []) {
  if (!e.geojson) continue;
  const gj = JSON.parse(e.geojson);    // CAD 坐标 GeoJSON（e.geom 不存在！）
  // mapext: featuresToEntities 期望 CAD 坐标 → 直接构 Feature（见 cad-mapper.md §六）
  // vjmap 叠加: map.toLngLat(gj) 转渲染经纬度
}
```

## 五、易错点

1. `toMapCoordinate:true` 后去读 `result[i].geom` → `undefined`；该 `JSON.parse(result[i].geojson)`。
2. 默认查询的 `.geom` 已是渲染经纬度，**不要再 `map.toLngLat`**——二次转换。
3. `points`/`location`/`bounds` 字符串是 CAD 坐标（不是经纬度），split 后若要上图须 `toLngLat`。
4. **`envelop` 是 WKT 字符串且坐标是原始墨卡托**（负的百万级数值），不是 CAD——要包络用 `bounds` 字段（CAD `"[x1,y1,x2,y2]"`）或 `map.getEnvelopBounds(envelop)`（内部 `prj.fromMercator` 转成 CAD）。
5. **不传 `includegeom`/`realgeom`，多条结果的 `geojson` 是空串** → `.geom` 不生成、只有 envelop/bounds/location。要真实几何必传。
6. 查询落空时没有 `result`（条件查询为 `{recordCount:0}`，点/矩形查询多为 `{}`）——`res.result ?? []` 兜底。
7. 互联网底图上跑 `conditionQueryFeature`（查某张 CAD 图）：`.geom` 默认转出的是**该 CAD 图的渲染经纬度**，不是你的互联网底图经纬度——要叠到互联网底图请用 `toMapCoordinate:true` 拿 CAD 坐标，再走 `cmdTransform`→`GCJ02/WGS84`（见 [internet-basemap-align](internet-basemap-align.md) §四之三）。
8. `cb` 回调（四个查询接口都有：`pointQueryFeature(param, cb)`、`rectQueryFeature`、`conditionQueryFeature`、`exprQueryFeature`；另外 `processQueryResult(param, cb)` 与 `cmdCreateEntitiesGeomData(param, cb)` 也走同一处理管线，签名 `(point:[x,y]) => [x,y]|null|undefined`）可对每个坐标点自定义处理，**返回非空值就跳过 SDK 的默认转换**——这是"自定义结果坐标系"的钩子（比如直接在这里做 CAD→GCJ02）。
9. **查询入参 `x/y`/`bounds` 传渲染经纬度** → 入参要 CAD：`map.fromLngLat(e.lngLat)` 先转。经纬度值（如 116,39）当 CAD 传给点查询 → `recordCount:0`。
10. `enableLayerClickHighlight(svc, cb)` 内部已做 `fromLngLat` → 点查询 → `.geom` 回填到高亮 source，全程无需手动转。
