"我能在这儿租房吗?"这不是一个问题,而是五个问题套着一件风衣。哪个选区?哪个分区?最近的湖有多远?医院够近吗?学校够近吗?在海得拉巴,今天要回答这些,得先打开一份市政PDF、一张选区地图、一个地图应用量距离,最后还是凭感觉拍板。

打开网易新闻 查看精彩图片

Catchment 想用单个形状把这一整套问题一次答完。点一个位置,或者画一片区域,它返回一个结论,外加每条规则背后真实的米数。然后它把问题反过来:把规则交给它一次,它就在整座城市里搜出所有符合条件的地点。

这是一个空间数据库项目,不是电子表格加循环。最朴素的版本是一个装着多边形的 JSON 文件加一个循环,对 145 个选区够用;可一旦要问"这个点 500 米内有什么",对象变成 2,566 个地点,查询就变成了浏览器必须硬扛的计算。项目的立场是:距离、包含关系和阈值比较,本来就该由数据库来回答。

于是整个项目被刻意塑造成围绕 Appwrite TablesDB 最少人用的那个角落:多边形列和点列、空间索引,以及 intersects 和 distanceLessThan 两个操作符。这个选择有代价——应用没法对数字的来源撒谎,因为产生这些数字的查询本身就是答案的一部分。这反倒成了产品里最好的特性。

数据全部从源头重建:DataMeet 与海得拉巴大市政公司提供的 145 个选区、5 个分区和城市边界,源自 OpenStreetMap;320 个水体和 2,566 个地点(医院、诊所、学校、学院、车站)经 Overpass API 取自 OpenStreetMap;另有 1,624 个候选网格单元,每个单元在构建时就对水、医院、学校和交通做了测量。底图来自 OpenFreeMap,地理编码来自 Nominatim,两者都不需要密钥。

数据库结构是七张表、十二个索引,其中五个是空间索引。承重的决定在水体表上:每个湖的精确几何存在 polys_json 里,boundary 列则存所有部分的凸包。这个拆分是被下面第一个坑逼出来的。

坑一:多边形太大,查询就哑了。Osman Sagar 的环有约 200 个点,查询直接不返回结果。解法是在写入时算凸包,环从约 200 个点降到 16 个点,查询才开始应答。

坑二:距离操作符是给点用的。在多边形列上执行 distanceLessThan,它不报错,而是返回零行。这一点值得读两遍:如果你的代码在数据库返回空时悄悄在本地补算,应用会在你机器上正确,在别人那里微妙地错,而且除非专门写测试,没人会发现。正确做法是问多边形列它能回答的问题——相交,再用第二个空间索引补一次中心点距离查询。

坑三:相交基于边界,不是包含。查询多边形被当作一个轮廓,完全落在 500 米圆内的池塘永远碰不到圆的边缘,所以单靠 intersects 查不到它。要两个问题都问、合并结果、再精确测量。测试用例很小,因此没有歧义:一个 0.026 平方公里的池塘 Erra Kunta,测试点在其以东 250 米,搜索半径 500 米。相交查询返回空,中心查询在 157 米处返回它,应用取的是并集。

坑四:小结构细节否掉大功能。空间索引不能建在可空列上,几何列若是可选的,建索引会以 column_index_invalid 失败,修法是把列标为必填再回填。批量写入接口接收的是行对象,不是 { rowId, data } 包装,第一次加载器写了包装,接口回"缺少必填属性 name",因为一个属性都没看到。还有一个云端的坑:Appwrite Cloud 项目被钉在单一区域,端点错了会回 401,提示项目在该区域不可访问。

界面上的约束不是装饰。需求是 70 岁和 17 岁的人都不用人教就能用,这变成了工程约束:17px 基准字号、三档文字大小,放大不破版;46 到 56px 的点击目标,44px 硬下限由脚本在五种视口宽度下审计;对比度按 AA 实测而非目测;没有纯图标按钮,没有长按,没有隐藏手势,每个动作都是一个词;通过和失败用文字写明,绝不只靠颜色;首次访问提示在第一次使用后消失;结论卡片在手机上自动滚入视野,因为折叠线以下的结果等于没人看见。无障碍回归在这里是构建失败,不是打磨项。

每个结论都带一张收据。每条结果下面有个"显示"控件,列出表名、操作符和这个数字背后的毫秒数。这不是调试模式,这是产品的论点:距离是真的,边界是真的,产生它的查询就在这里。这也让项目变得难以造假地可测——13 项检查对着线上项目跑,每项打印耗时,任何一项失败就以非零码退出。比如:Hussain Sagar 附近的水体,118 毫秒,1 行,最近的 Hussain Sagar 距离 0 米;完全落在搜索圆内的池塘,357 毫秒,Erra Kunta 在 157 米;Charminar 两公里内的医院和诊所,185 毫秒,43 行;距水至少 300 米且在一公里内有医院的网格,256 毫秒,1,624 个中的 1,031 个;与手绘环重叠的选区,116 毫秒,6 行,包括第 91 选区和第 88 选区。

这些输出就是演示。不是因为它们证明应用能用,而是因为它们说明了这是什么类型的应用。代码是 MIT 许可,数据是 ODbL,学得最久的那几部分都写进了 README。要在 TablesDB 上做任何空间相关的东西,从上面四个坑起步,能省下一个晚上。