spa-l2-cairo-shop-knn
Map (reference ← swipe → agent)
0:00
I'm putting together a retail-density readout for a downtown Cairo brief. `cairo_retail` has two layers, `shops` and `anchors`. For each anchor I need the 5 nearest shops with distance in metres and a within-1 km flag, plus a 5x3 distance matrix from those 5 shops to the anchor's 3 closest sibling anchors. The chain names in the source data have inconsistent spellings, so the output should use consistent names: the same `shop_id` always carries the same `normalised_name`, and all shops belonging to one chain share a single canonical spelling. Tidy the anchor names too.
Please write `market_neighbourhoods.json` as a top-level JSON array with one record per anchor. Each record carries the keys `anchor_id`, `anchor_name_normalised` (non-empty string), `knn` (exactly 5 entries sorted by ascending `distance_m`), and `full_distance_matrix_m` (5 rows by 3 numeric columns; rows follow the knn order, columns correspond to the sibling anchors in distance-ascending order). Each `knn` entry has `shop_id`, `normalised_name`, `distance_m` (finite numeric, in metres), and `within_1km` (boolean, true when `distance_m` is at most 1000).
0:00
System info
{
"adapter": "openrouter",
"model": "google/gemma-4-26b-a4b-it",
"base_url": "https://openrouter.ai/api/v1"
}
0:01
Bash
ls -F
cairo_retail.gpkg
0:02
Bash
ogrinfo -al /work/cairo_retail.gpkg
INFO: Open of `/work/cairo_retail.gpkg'
using driver `GPKG' successful.
Layer name: shops
Geometry: Point
Feature Count: 10000
Extent: (634124.820679, 804501.897982) - (653413.229591, 826664.941539)
Layer SRS WKT:
PROJCRS["Egypt 1907 / Red Belt",
BASEGEOGCRS["Egypt 1907",
DATUM["Egypt 1907",
ELLIPSOID["Helmert 1906",6378200,298.3,
LENGTHUNIT["metre",1]]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
ID["EPSG",4229]],
CONVERSION["Egypt Red Belt",
METHOD["Transverse Mercator",
ID["EPSG",9807]],
PARAMETER["Latitude of natural origin",30,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8801]],
PARAMETER["Longitude of natural origin",31,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8802]],
PARAMETER["Scale factor at natural origin",1,
SCALEUNIT["unity",1],
ID["EPSG",8805]],
PARAMETER["False easting",615000,
LENGTHUNIT["metre",1],
ID["EPSG",8806]],
PARAMETER["False northing",810000,
LENGTHUNIT["metre",1],
ID["EPSG",8807]]],
CS[Cartesian,2],
AXIS["(E)",east,
ORDER[1],
LENGTHUNIT["metre",1]],
AXIS["(N)",north,
ORDER[2],
LENGTHUNIT["metre",1]],
USAGE[
SCOPE["Engineering survey, topographic mapping."],
AREA["Egypt - onshore between 29°E and 33°E, offshore Mediterranean east of 29°E and offshore Gulf of Suez."],
BBOX[21.99,29,33.82,34.27]],
ID["EPSG",22992]]
Data axis to CRS axis mapping: 1,2
FID Column = fid
Geometry Column = geom
shop_id: String (0.0)
raw_name: String (0.0)
OGRFeature(shops):1
shop_id (String) = S00001
raw_name (String) = OSCAR
POINT (637478.058487924 814957.397675864)
OGRFeature(shops):2
shop_id (String) = S00002
raw_name (String) = Local Shop 00002
POINT (651773.775467847 816149.978750284)
OGRFeature(shops):3
shop_id (String) = S00003
raw_name (String) = Local Shop 00003
POINT (640341.689388647 815148.396308898)
OGRFeature(shops):4
shop_id (String) = S00004
raw_name (String) = Local Shop 00004
POINT (640225.465313055 805311.132268022)
OGRFeature(shops):5
shop_id (String) = S00005
raw_name (String) = Local Shop 00005
POINT (647199.272147092 818195.958649989)
OGRFeature(shops):6
shop_id (String) = S00006
raw_name (String) = مترو
POINT (647363.601317037 817078.396547119)
OGRFeature(shops):7
shop_id (String) = S00007
raw_name (String) = Local Shop 00007
POINT (637452.603531365 813452.72091361)
OGRFeature(shops):8
shop_id (String) = S00008
raw_name (String) = Carrefour
POINT (642884.748634319 820068.563638251)
OGRFeature(shops):9
shop_id (String) = S00009
raw_name (String) = Local Shop 00009
POINT (638401.854261998 815382.513480527)
OGRFeature(shops):10
shop_id (String) = S00010
raw_name (String) = Carrefour
POINT (645066.080274557 808334.791083099)
OGRFeature(shops):11
shop_id (String) = S00011
raw_name (String) = Local Shop 00011
POINT (643741.745157574 821785.240227738)
OGRFeature(shops):12
shop_id (String) = S00012
raw_name (String) = سعودي
POINT (644818.535108658 821149.698078065)
OGRFeature(shops):13
shop_id (String) = S00013
raw_name (String) = Local Shop 00013
POINT (650857.192266837 824183.112555852)
OGRFeature(shops):14
shop_id (String) = S00014
raw_name (String) = Local Shop 00014
POINT (638852.939982444 816104.428331507)
OGRFeature(shops):15
shop_id (String) = S00015
raw_name (String) = Local Shop 00015
POINT (647652.575844648 814307.8050582)
OGRFeature(shops):16
shop_id (String) = S00016
raw_name (String) = Spinneys
POINT (634460.575376554 810550.892087218)
OGRFeature(shops):17
shop_id (String) = S00017
raw_name (String) = Local Shop 00017
POINT (635587.163773858 814404.568685004)
OGRFeature(shops):18
shop_id (String) = S00018
raw_name (String) = Local Shop 00018
POINT (639796.624798416 816106.1176905)
OGRFeature(shops):19
shop_id (String) = S00019
raw_name (String) = Local Shop 00019
POINT (650413.267683925 823729.980828671)
OGRFeature(shops):20
shop_id (String) = S00020
raw_name (String) = Local Shop 00020
POINT (636848.935390639 814863.407903552)
OGRFeature(shops):21
shop_id (String) = S00021
raw_name (String) = Carrefour Egypt
POINT (645992.624943993 820312.549167156)
OGRFeature(shops):22
shop_id (String) = S00022
raw_name (String) = carrefour
POINT (639307.514299775 814383.71812242)
OGRFeature(shops):23
shop_id (String) = S00023
raw_name (String) = Local Shop 00023
POINT (642137.39677221 818942.648728208)
OGRFeature(shops):24
shop_id (String) = S00024
raw_name (String) = Local Shop 00024
POINT (647939.256790264 820996.284100536)
OGRFeature(shops):25
shop_id (String) = S00025
raw_name (String) = Local Shop 00025
POINT (636845.403996236 817961.517214108)
OGRFeature(shops):26
shop_id (String) = S00026
raw_name (String) = Seoudi Market
POINT (640745.308517588 817734.951406899)
OGRFeature(shops):27
shop_id (String) = S00027
raw_name (String) = Local Shop 00027
POINT (636914.1650506 811626.618509781)
OGRFeature(shops):28
shop_id (String) = S00028
raw_name (String) = Hyper One
POINT (645400.803003731 822380.568640866)
OGRFeature(shops):29
shop_id (String) = S00029
raw_name (String) = أبو زكري
POINT (651120.617150177 821806.250641437)
OGRFeature(shops):30
shop_id (String) = S00030
raw_name (String) = Local Shop 00030
POINT (649654.934704055 825045.327781002)
OGRFeature(shops):31
shop_id (String) = S00031
raw_name (String) = Local Shop 00031
POINT (645234.472923498 821521.316476857)
OGRFeature(shops):32
shop_id (String) = S00032
raw_name (String) = سبينيز
POINT (653201.717802904 826046.011867956)
OGRFeature(shops):33
shop_id (String) = S00033
raw_name (String) = Local Shop 00033
POINT (640143.021333039 806893.158943572)
OGRFeature(shops):34
shop_id (String) = S00034
raw_name (String) = Local Shop 00034
POINT (634220.24606244 814230.919050761)
OGRFeature(shops):35
shop_id (String) = S00035
raw_name (String) = Local Shop 00035
POINT (641185.051175902 818686.505275732)
OGRFeature(shops):36
shop_id (String) = S00036
raw_name (String) = Local Shop 00036
POINT (647445.148557739 815390.030194781)
OGRFeature(shops):37
shop_id (String) = S00037
raw_name (String) = Local Shop 00037
POINT (642353.25356077 811539.845918871)
OGRFeature(shops):38
shop_id (String) = S00038
raw_name (String) = Local Shop 00038
POINT (639024.246853763 806973.805950547)
OGRFeature(shops):39
shop_id (String) = S00039
raw_name (String) = Metro Markets
POINT (634296.071778366 815940.224664069)
OGRFeature(shops):40
shop_id (String) = S00040
raw_name (String) = Local Shop 00040
POINT (647839.797174084 819881.662878391)
OGRFeature(shops):41
shop_id (String) = S00041
raw_name (String) = Local Shop 00041
POINT (642159.910221419 822242.371250848)
OGRFeature(shops):42
shop_id (String) = S00042
raw_name (String) = Seoudi Market
POINT (648212.720482228 814081.216722253)
OGRFeature(shops):43
shop_id (String) = S00043
raw_name (String) = seoudi supermarket
POINT (639554.942020051 815744.568750557)
OGRFeature(shops):44
shop_id (String) = S00044
raw_name (String) = كارفور
POINT (640126.116557115 805053.313967132)
OGRFeature(shops):45
shop_id (String) = S00045
raw_name (String) = Seoudi Market
POINT (638735.343553551 819982.238237748)
OGRFeature(shops):46
shop_id (String) = S00046
raw_name (String) = Kheir Zaman
POINT (639213.525535796 821591.67361862)
OGRFeature(shops):47
shop_id (String) = S00047
raw_name (String) = مترو
POINT (637736.840266277 815347.148521926)
OGRFeature(shops):48
shop_id (String) = S00048
raw_name (String) = Local Shop 00048
POINT (646506.495534073 815898.228078493)
OGRFeature(shops):49
shop_id (String) = S00049
raw_name (String) = Local Shop 00049
POINT (645294.460107304 807162.265522615)
OGRFeature(shops):50
shop_id (String) = S00050
raw_name (String) = Local Shop 00050
POINT (642609.624706283 805053.575899187)
OGRFeature(shops):51
shop_id (String) = S00051
raw_name (String) = Spinneys
POINT (635260.828189161 809274.201978989)
OGRFeature(shops):52
shop_id (String) = S00052
raw_name (String) = Local Shop 00052
POINT (637559.978772543 813924.078892127)
OGRFeature(shops):53
shop_id (String) = S00053
raw_name (String) = Local Shop 00053
POINT (641666.880297001 817560.974840412)
OGRFeature(shops):54
shop_id (String) = S00054
raw_name (String) = اوسكار
POINT (643893.093046164 819843.010013626)
OGRFeature(shops):55
shop_id (String) = S00055
raw_name (String) = Local Shop 00055
POINT (640849.07951945 804547.430194517)
OGRFeature(shops):56
shop_id (String) = S00056
raw_name (String) = Local Shop 00056
POINT (650635.472219923 814276.972137346)
OGRFeature(shops):57
shop_id (String) = S00057
raw_name (String) = Carrefour Egypt
POINT (651539.838906531 816504.475099993)
OGRFeature(shops):58
shop_id (String) = S00058
raw_name (String) = Spineys
POINT (644121.283363534 820085.70171023)
OGRFeature(shops):59
shop_id (String) = S00059
raw_name (String) = Local Shop 00059
POINT (636550.691587081 811882.16719895)
OGRFeature(shops):60
shop_id (String) = S00060
raw_name (String) = اوسكار
POINT (647677.314110989 814338.137693293)
OGRFeature(shops):61
shop_id (String) = S00061
raw_name (String) = خير زمان
POINT (647846.933009284 822407.582068612)
OGRFeature(shops):62
shop_id (String) = S00062
raw_name (String) = Seoudi
POINT (647137.91190756 816135.872012627)
OGRFeature(shops):63
shop_id (String) = S00063
raw_name (String) = Local Shop 00063
POINT (639398.018282206 805607.597990786)
OGRFeature(shops):64
shop_id (String) = S00064
raw_name (String) = Spinneys
POINT (642113.564066383 821776.742038064)
OGRFeature(shops):65
shop_id (String) = S00065
raw_name (String) = kheir zaman
POINT (638080.248996985 815616.584832917)
OGRFeature(shops):66
shop_id (String) = S00066
raw_name (String) = Metro Markets
POINT (635495.067490723 817073.324157865)
OGRFeature(shops):67
shop_id (String) = S00067
raw_name (String) = Local Shop 00067
POINT (647792.570615126 823473.840426822)
OGRFeature(shops):68
shop_id (String) = S00068
raw_name (String) = Metro Market
POINT (642394.680454836 812067.142725599)
OGRFeature(shops):69
shop_id (String) = S00069
raw_name (String) = Local Shop 00069
POINT (634414.285380876 817275.398639876)
OGRFeature(shops):70
shop_id (String) = S00070
raw_name (String) = Carrefour Egypt
POINT (635901.646970307 813443.53309074)
OGRFeature(shops):71
shop_id (String) = S00071
raw_name (String) = Local Shop 00071
POINT (647536.593510533 825036.026203852)
OGRFeature(shops):72
shop_id (String) = S00072
raw_name (String) = Local Shop 00072
POINT (635614.830714245 818759.084433544)
OGRFeature(shops):73
shop_id (String) = S00073
raw_name (String) = Carrefour
POINT (644993.652256943 819891.894431124)
OGRFeature(shops):74
shop_id (String) = S00074
raw_name (String) = Metro Market
POINT (635591.864231081 809012.205494479)
OGRFeature(shops):75
shop_id (String) = S00075
raw_name (String) = Local Shop 00075
POINT (645632.943132826 823889.456276904)
OGRFeature(shops):76
shop_id (String) = S00076
raw_name (String) = Local Shop 00076
POINT (638726.785918316 822885.809224467)
OGRFeature(shops):77
shop_id (String) = S00077
raw_name (String) = Local Shop 00077
POINT (639203.849629931 806972.466961293)
OGRFeature(shops):78
shop_id (String) = S00078
raw_name (String) = Seoudi
POINT (635049.143874879 816024.314888034)
OGRFeature(shops):79
shop_id (String) = S00079
raw_name (String) = Local Shop 00079
POINT (645727.5822966 824173.504482637)
OGRFeature(shops):80
shop_id (String) = S00080
raw_name (String) = Local Shop 00080
POINT (635745.421464002 814347.998185277)
OGRFeature(shops):81
shop_id (String) = S00081
raw_name (String) = Local Shop 00081
POINT (635965.716554733 817635.196727522)
OGRFeature(shops):82
shop_id (String) = S00082
raw_name (String) = Local Shop 00082
POINT (637364.614006502 813842.908207791)
OGRFeature(shops):83
shop_id (String) = S00083
raw_name (String) = Abu Zekry
POINT (639951.183564329 823097.206411547)
OGRFeature(shops):84
shop_id (String) = S00084
raw_name (String) = Local Shop 00084
POINT (646880.380156415 820564.31034136)
OGRFeature(shops):85
shop_id (String) = S00085
raw_name (String) = Local Shop 00085
POINT (640468.159893446 823189.55523023)
OGRFeature(shops):86
shop_id (String) = S00086
raw_name (String) = Local Shop 00086
POINT (650856.888250377 818827.462567867)
OGRFeature(shops):87
shop_id (String) = S00087
raw_name (String) = Metro Market
POINT (643259.70699192 819700.63474687)
OGRFeature(shops):88
shop_id (String) = S00088
raw_name (String) = Local Shop 00088
POINT (646884.583792702 816767.236782552)
OGRFeature(shops):89
shop_id (String) = S00089
raw_name (String) = abou zekry
POINT (641246.345782533 816997.192330094)
OGRFeature(shops):90
shop_id (String) = S00090
raw_name (String) = Local Shop 00090
POINT (639437.536153606 806437.046899634)
OGRFeature(shops):91
shop_id (String) = S00091
raw_name (String) = Local Shop 00091
POINT (646236.069973403 820103.136384075)
OGRFeature(shops):92
shop_id (String) = S00092
raw_name (String) = Local Shop 00092
POINT (647574.17153671 818773.379679601)
OGRFeature(shops):93
shop_id (String) = S00093
raw_name (String) = Local Shop 00093
POINT (642238.457050183 821122.432278458)
OGRFeature(shops):94
shop_id (String) = S00094
raw_name (String) = Oscar Grand Stores
POINT (636738.611442621 811775.42134796)
OGRFeature(shops):95
shop_id (String) = S00095
raw_name (String) = Local Shop 00095
POINT (639230.502329634 815712.056518643)
OGRFeature(shops):96
shop_id (String) = S00096
raw_name (String) = Local Shop 00096
POINT (651081.784028535 821053.183619872)
OGRFeature(shops):97
shop_id (String) = S00097
raw_name (String) = metro
POINT (648008.514442601 821846.01246577)
OGRFeature(shops):98
shop_id (String) = S00098
raw_name (String) = Carrefour
POINT (646335.786242196 821288.867309655)
OGRFeature(shops):99
shop_id (String) = S00099
raw_name (String) = abou zekry
POINT (637912.954125579 815062.619568849)
OGRFeature(shops):100
shop_id (String) = S00100
raw_name (String) = Oscar Grand Stores
POINT (646984.425046821 823005.860642439)
OGRFeature(shops):101
shop_id (String) = S00101
raw_name (String) = Carrefour
POINT (638464.536455515 816188.790674229)
OGRFeature(shops):102
shop_id (String) = S00102
raw_name (String) = Seoudi
POINT (638164.3
[... truncated ...],0.0174532925199433],
ID["EPSG",8801]],
PARAMETER["Longitude of natural origin",31,
ANGLEUNIT["degree",0.0174532925199433],
ID["EPSG",8802]],
PARAMETER["Scale factor at natural origin",1,
SCALEUNIT["unity",1],
ID["EPSG",8805]],
PARAMETER["False easting",615000,
LENGTHUNIT["metre",1],
ID["EPSG",8806]],
PARAMETER["False northing",810000,
LENGTHUNIT["metre",1],
ID["EPSG",8807]]],
CS[Cartesian,2],
AXIS["(E)",east,
ORDER[1],
LENGTHUNIT["metre",1]],
AXIS["(N)",north,
ORDER[2],
LENGTHUNIT["metre",1]],
USAGE[
SCOPE["Engineering survey, topographic mapping."],
AREA["Egypt - onshore between 29°E and 33°E, offshore Mediterranean east of 29°E and offshore Gulf of Suez."],
BBOX[21.99,29,33.82,34.27]],
ID["EPSG",22992]]
Data axis to CRS axis mapping: 1,2
FID Column = fid
Geometry Column = geom
anchor_id: String (0.0)
anchor_name: String (0.0)
OGRFeature(anchors):1
anchor_id (String) = M001
anchor_name (String) = TAHRIR SQUARE PLAZA
POINT (635109.342648756 805565.909388724)
OGRFeature(anchors):2
anchor_id (String) = M002
anchor_name (String) = Zamalek Riverside
POINT (637039.843142311 805569.596603016)
OGRFeature(anchors):3
anchor_id (String) = M003
anchor_name (String) = Maadi Corniche
POINT (638970.344989423 805573.620349565)
OGRFeature(anchors):4
anchor_id (String) = M004
anchor_name (String) = HELIOPOLIS SQUARE
POINT (640900.848308882 805577.980630096)
OGRFeature(anchors):5
anchor_id (String) = M005
anchor_name (String) = New Cairo Tagamoa
POINT (642831.353219476 805582.677446487)
OGRFeature(anchors):6
anchor_id (String) = M006
anchor_name (String) = Nasr City Hub
POINT (644761.859839994 805587.71080076)
OGRFeature(anchors):7
anchor_id (String) = M007
anchor_name (String) = MOHANDESSIN CENTRE
POINT (646692.368289223 805593.08069508)
OGRFeature(anchors):8
anchor_id (String) = M008
anchor_name (String) = Garden City Walk
POINT (648622.878685952 805598.787131757)
OGRFeature(anchors):9
anchor_id (String) = M009
anchor_name (String) = Dokki Market
POINT (650553.391148969 805604.830113248)
OGRFeature(anchors):10
anchor_id (String) = M010
anchor_name (String) = GIZA PYRAMIDS GATE
POINT (652483.90579706 805611.209642154)
OGRFeature(anchors):11
anchor_id (String) = M011
anchor_name (String) = Roxy Square
POINT (635105.300171254 807782.94910312)
OGRFeature(anchors):12
anchor_id (String) = M012
anchor_name (String) = Ramses Crossing
POINT (637035.414065736 807786.637802769)
OGRFeature(anchors):13
anchor_id (String) = M013
anchor_name (String) = SAYEDA ZEINAB PLAZA
POINT (638965.529311871 807790.663171011)
OGRFeature(anchors):14
anchor_id (String) = M014
anchor_name (String) = Khan El Khalili Approach
POINT (640895.646028282 807795.025209573)
OGRFeature(anchors):15
anchor_id (String) = M015
anchor_name (String) = Bab Al Louq Corner
POINT (642825.764333592 807799.723920335)
OGRFeature(anchors):16
anchor_id (String) = M016
anchor_name (String) = KORBA QUARTER
POINT (644755.884346419 807804.759305316)
OGRFeature(anchors):17
anchor_id (String) = M017
anchor_name (String) = Manial Riverbank
POINT (646686.006185388 807810.131366678)
OGRFeature(anchors):18
anchor_id (String) = M018
anchor_name (String) = Shubra North
POINT (648616.129969119 807815.840106739)
OGRFeature(anchors):19
anchor_id (String) = M019
anchor_name (String) = AIN SHAMS PLAZA
POINT (650546.255816234 807821.885527948)
OGRFeature(anchors):20
anchor_id (String) = M020
anchor_name (String) = Abbasiya Junction
POINT (652476.383845353 807828.26763291)
OGRFeature(anchors):21
anchor_id (String) = M021
anchor_name (String) = Boulaq Edge
POINT (635101.255244391 809999.995552928)
OGRFeature(anchors):22
anchor_id (String) = M022
anchor_name (String) = GARBIYA PLAZA
POINT (637030.982304631 810003.685736143)
OGRFeature(anchors):23
anchor_id (String) = M023
anchor_name (String) = Sakakini Approach
POINT (638960.71071462 810007.712724123)
OGRFeature(anchors):24
anchor_id (String) = M024
anchor_name (String) = Dar El Salaam
POINT (640890.440592813 810012.076518601)
OGRFeature(anchors):25
anchor_id (String) = M025
anchor_name (String) = EL MARG HUB
POINT (642820.172057666 810016.77712145)
OGRFeature(anchors):26
anchor_id (String) = M026
anchor_name (String) = Helwan Centre
POINT (644749.905227635 810021.814534693)
OGRFeature(anchors):27
anchor_id (String) = M027
anchor_name (String) = Maasara Crossing
POINT (646679.640221173 810027.188760494)
OGRFeature(anchors):28
anchor_id (String) = M028
anchor_name (String) = TORA EDGE
POINT (648609.377156735 810032.899801163)
OGRFeature(anchors):29
anchor_id (String) = M029
anchor_name (String) = Mokattam Heights
POINT (650539.116152777 810038.947659156)
OGRFeature(anchors):30
anchor_id (String) = M030
anchor_name (String) = Nozha Promenade
POINT (652468.857327752 810045.332337074)
OGRFeature(anchors):31
anchor_id (String) = M031
anchor_name (String) = SHERATON HELIOPOLIS
POINT (635097.207868641 812217.0487409)
OGRFeature(anchors):32
anchor_id (String) = M032
anchor_name (String) = Triumph Square
POINT (637026.547859514 812220.740405891)
OGRFeature(anchors):33
anchor_id (String) = M033
anchor_name (String) = Cleopatra Plaza
POINT (638955.889198232 812224.769011655)
OGRFeature(anchors):34
anchor_id (String) = M034
anchor_name (String) = SALAH SALEM STRIP
POINT (640885.232003083 812229.13455993)
OGRFeature(anchors):35
anchor_id (String) = M035
anchor_name (String) = Autostrad Corner
POINT (642814.576392355 812233.837052586)
OGRFeature(anchors):36
anchor_id (String) = M036
anchor_name (String) = El Rehab Gate One
POINT (644743.922484339 812238.876491647)
OGRFeature(anchors):37
anchor_id (String) = M037
anchor_name (String) = EL REHAB GATE TWO
POINT (646673.270397321 812244.252879275)
OGRFeature(anchors):38
anchor_id (String) = M038
anchor_name (String) = Madinaty Promenade
POINT (648602.62024959 812249.966217781)
OGRFeature(anchors):39
anchor_id (String) = M039
anchor_name (String) = Fifth Settlement North
POINT (650531.972159432 812256.01650962)
OGRFeature(anchors):40
anchor_id (String) = M040
anchor_name (String) = FIFTH SETTLEMENT SOUTH
POINT (652461.326245137 812262.403757393)
OGRFeature(anchors):41
anchor_id (String) = M041
anchor_name (String) = American University Gate
POINT (635093.158044477 814434.108669789)
OGRFeature(anchors):42
anchor_id (String) = M042
anchor_name (String) = Police Academy Strip
POINT (637022.110730905 814437.801814759)
OGRFeature(anchors):43
anchor_id (String) = M043
anchor_name (String) = RING ROAD NORTH
POINT (638951.064763271 814441.832036356)
OGRFeature(anchors):44
anchor_id (String) = M044
anchor_name (String) = Ring Road East
POINT (640880.020259701 814446.199336309)
OGRFeature(anchors):45
anchor_id (String) = M045
anchor_name (String) = Ring Road West
POINT (642808.977338314 814450.903716488)
OGRFeature(anchors):46
anchor_id (String) = M046
anchor_name (String) = CITY STARS MALL
POINT (644737.936117233 814455.945178918)
OGRFeature(anchors):47
anchor_id (String) = M047
anchor_name (String) = Cairo Festival City
POINT (646666.896714578 814461.323725762)
OGRFeature(anchors):48
anchor_id (String) = M048
anchor_name (String) = Mall of Egypt Gate
POINT (648595.859248473 814467.039359331)
OGRFeature(anchors):49
anchor_id (String) = M049
anchor_name (String) = TAGAMOA FIRST
POINT (650524.823837037 814473.092082079)
OGRFeature(anchors):50
anchor_id (String) = M050
anchor_name (String) = Tagamoa Third
POINT (652453.790598391 814479.481896607)
OGRFeature(anchors):51
anchor_id (String) = M051
anchor_name (String) = El Mokattam Plateau
POINT (635089.105772372 816651.175342334)
OGRFeature(anchors):52
anchor_id (String) = M052
anchor_name (String) = AL AHLY STADIUM
POINT (637017.67091932 816654.869965498)
OGRFeature(anchors):53
anchor_id (String) = M053
anchor_name (String) = Cairo Stadium
POINT (638946.237410303 816658.901800969)
OGRFeature(anchors):54
anchor_id (String) = M054
anchor_name (String) = Sharkawi Plaza
POINT (640874.805363277 816663.270850477)
OGRFeature(anchors):55
anchor_id (String) = M055
anchor_name (String) = EL OBOUR HUB
POINT (642803.374896195 816667.977115899)
OGRFeature(anchors):56
anchor_id (String) = M056
anchor_name (String) = Shoubra Mazallat
POINT (644731.946127015 816673.020599251)
OGRFeature(anchors):57
anchor_id (String) = M057
anchor_name (String) = Abdeen Palace Edge
POINT (646660.51917369 816678.401302701)
OGRFeature(anchors):58
anchor_id (String) = M058
anchor_name (String) = EL HUSSEIN SQUARE
POINT (648589.094154174 816684.119228557)
OGRFeature(anchors):59
anchor_id (String) = M059
anchor_name (String) = Al Ghouriya Strip
POINT (650517.671186424 816690.174379273)
OGRFeature(anchors):60
anchor_id (String) = M060
anchor_name (String) = El Mosky Quarter
POINT (652446.250388393 816696.566757451)
OGRFeature(anchors):61
anchor_id (String) = M061
anchor_name (String) = BAB ZUWEILA APPROACH
POINT (635085.0510528 818868.248761288)
OGRFeature(anchors):62
anchor_id (String) = M062
anchor_name (String) = Ataba Square
POINT (637013.228425281 818871.944860848)
OGRFeature(anchors):63
anchor_id (String) = M063
anchor_name (String) = Opera Square
POINT (638941.407139891 818875.978308235)
OGRFeature(anchors):64
anchor_id (String) = M064
anchor_name (String) = TALAAT HARB PLAZA
POINT (640869.587314421 818880.349105182)
OGRFeature(anchors):65
anchor_id (String) = M065
anchor_name (String) = Soliman Pasha Corner
POINT (642797.769066654 818885.057253557)
OGRFeature(anchors):66
anchor_id (String) = M066
anchor_name (String) = Sherif Street
POINT (644725.952514387 818890.102755384)
OGRFeature(anchors):67
anchor_id (String) = M067
anchor_name (String) = QASR EL NILE
POINT (646654.137775401 818895.485612828)
OGRFeature(anchors):68
anchor_id (String) = M068
anchor_name (String) = Kasr El Aini Strip
POINT (648582.324967487 818901.205828196)
OGRFeature(anchors):69
anchor_id (String) = M069
anchor_name (String) = El Sayeda Aisha
POINT (650510.514208432 818907.263403939)
OGRFeature(anchors):70
anchor_id (String) = M070
anchor_name (String) = KOBRI EL QUBBA
POINT (652438.705616025 818913.658342663)
OGRFeature(anchors):71
anchor_id (String) = M071
anchor_name (String) = Mar Mina Plaza
POINT (635080.993886236 821085.328929381)
OGRFeature(anchors):72
anchor_id (String) = M072
anchor_name (String) = Saint Fatima Hub
POINT (637008.783249307 821089.026503546)
OGRFeature(anchors):73
anchor_id (String) = M073
anchor_name (String) = EL NOZHA EL GEDIDA
POINT (638936.5739526 821093.061560896)
OGRFeature(anchors):74
anchor_id (String) = M074
anchor_name (String) = Rabaa Square
POINT (640864.366113741 821097.434103156)
OGRFeature(anchors):75
anchor_id (String) = M075
anchor_name (String) = Tagamoa El Saba
POINT (642792.159850349 821102.144132202)
OGRFeature(anchors):76
anchor_id (String) = M076
anchor_name (String) = BAHTEEM CROSSING
POINT (644719.955280048 821107.191650056)
OGRFeature(anchors):77
anchor_id (String) = M077
anchor_name (String) = El Salam City
POINT (646647.752520459 821112.576658876)
OGRFeature(anchors):78
anchor_id (String) = M078
anchor_name (String) = Madinet Nasr Eighth Zone
POINT (648575.551689202 821118.299160976)
OGRFeature(anchors):79
anchor_id (String) = M079
anchor_name (String) = MADINET NASR TENTH ZONE
POINT (650503.352903899 821124.359158809)
OGRFeature(anchors):80
anchor_id (String) = M080
anchor_name (String) = El Hadaba El Wosta
POINT (652431.15628217 821130.756654973)
OGRFeature(anchors):81
anchor_id (String) = M081
anchor_name (String) = Mokattam Sector One
POINT (635076.934273154 823302.415849354)
OGRFeature(anchors):82
anchor_id (String) = M082
anchor_name (String) = MOKATTAM SECTOR SIX
POINT (637004.335391916 823306.114896331)
OGRFeature(anchors):83
anchor_id (String) = M083
anchor_name (String) = El Maadi Degla
POINT (638931.737848996 823310.151561682)
OGRFeature(anchors):84
anchor_id (String) = M084
anchor_name (String) = Maadi Sarayat
POINT (640859.141761851 823314.525847135)
OGRFeature(anchors):85
anchor_id (String) = M085
anchor_name (String) = MAADI CORNISH
POINT (642786.547247934 823319.237754566)
OGRFeature(anchors):86
anchor_id (String) = M086
anchor_name (String) = Old Cairo Babylon
POINT (644713.954424702 823324.287285992)
OGRFeature(anchors):87
anchor_id (String) = M087
anchor_name (String) = Coptic Cairo Plaza
POINT (646641.36340961 823329.674443578)
OGRFeature(anchors):88
anchor_id (String) = M088
anchor_name (String) = FUSTAT PARK EDGE
POINT (648568.774320111 823335.399229632)
OGRFeature(anchors):89
anchor_id (String) = M089
anchor_name (String) = Manial Bridge
POINT (650496.187273661 823341.461646609)
OGRFeature(anchors):90
anchor_id (String) = M090
anchor_name (String) = Embaba Crossing
POINT (652423.602387713 823347.861697106)
OGRFeature(anchors):91
anchor_id (String) = M091
anchor_name (String) = IMBABA AIRPORT STRIP
POINT (635072.87221403 825519.509523944)
OGRFeature(anchors):92
anchor_id (String) = M092
anchor_name (String) = Mit Okba Plaza
POINT (636999.88485363 825523.210041937)
OGRFeature(anchors):93
anchor_id (String) = M093
anchor_name (String) = El Agouza Riverside
POINT (638926.898829644 825527.248313331)
OGRFeature(anchors):94
anchor_id (String) = M094
anchor_name (String) = EL SAHEL JUNCTION
POINT (640853.91425936 825531.624339853)
OGRFeature(anchors):95
anchor_id (String) = M095
anchor_name (String) = Rod El Farag Bridge
POINT (642780.931260066 825536.33812338)
OGRFeature(anchors):96
anchor_id (String) = M096
anchor_name (String) = Shubra El Kheima Centre
POINT (644707.94994905 825541.389665928)
OGRFeature(anchors):97
anchor_id (String) = M097
anchor_name (String) = EL SAWAH CORNER
POINT (646634.970443601 825546.778969663)
OGRFeature(anchors):98
anchor_id (String) = M098
anchor_name (String) = Demerdash Plaza
POINT (648561.992861007 825552.506036892)
OGRFeature(anchors):99
anchor_id (String) = M099
anchor_name (String) = El Demerdash Hospital Edge
POINT (650489.017318556 825558.570870069)
OGRFeature(anchors):100
anchor_id (String) = M100
anchor_name (String) = RAMSES HILTON PLAZA
POINT (652416.043933533 825564.973471791)
0:29
Write
/work/solve.py
content · 187 lines · py
import geopandas as gpd
import pandas as pd
import json
from shapely.ops import nearest_points
from scipy.spatial import cKDTree
import numpy as np
def normalize_name(name):
if not name or pd.isna(name):
return ""
name = str(name).strip().upper()
# Mapping common variations to a canonical name
# Based on the ogrinfo output, we see some patterns.
# Since we don't have the full list, we'll use a more robust approach.
# We'll group by some kind of "semantic" similarity or just a mapping.
# For this task, let's define a mapping for the obvious ones.
mapping = {
"CARREFOUR": "Carrefour",
"CARREFOUR EGYPT": "Carrefour",
"كارفور": "Carrefour",
"OSCAR": "Oscar",
"اوسكار": "Oscar",
"OSCAR GRAND STORES": "Oscar",
"SEOUDI MARKET": "Seoudi",
"SEOUDI": "Seoudi",
"سعودي": "Seoudi",
"SEOUDI SUPERMARKET": "Seoudi",
"SPINNEYS": "Spinneys",
"SPINEYS": "Spinneys",
"سبينيز": "Spinneys",
"METRO MARKETS": "Metro Markets",
"METRO MARKET": "Metro Markets",
"مترو": "Metro Markets",
"KHEIR ZAMAN": "Kheir Zaman",
"خير زمان": "Kheir Zaman"
}
# Check if the name is in our mapping (exact match after stripping/uppercasing)
if name in mapping:
return mapping[name]
# For names like "Local Shop 00002", let's just normalize to "Local Shop" or something?
# Actually, the instruction says: "the same shop_id always carries the same normalised_name,
# and all shops belonging to one chain share a single canonical spelling."
# And "Tidy the anchor names too."
# If it looks like "Local Shop XXXX", let's treat it as "Local Shop"
import re
if re.match(r"LOCAL SHOP \d+", name):
return "Local Shop"
# For Abu Zekry / Abou Zekry / أبو زكري
if "ABU ZE KRY" in name or "ABOU ZE KRY" in name or "أبو زكري" in name:
return "Abu Zekry"
return name
def solve():
# Load data
gdf_shops = gpd.read_file("/work/cairo_retail.gpkg", layer="shops")
gdf_anchors = gpd.read_file("/work/cairo_retail.gpkg", layer="anchors")
# 1. Normalize Shop Names
# We need to ensure that all shops in a chain share a canonical spelling.
# A good way is to create a dictionary: raw_name -> normalized_name.
# But we must also ensure that the same normalized_name is used for all shops.
# First, create a list of all raw names and try to group them.
# Given the prompt's specific mention of inconsistent spellings, we'll
# use a manual mapping for known chains and a fallback.
def get_canonical_name(raw_name):
name = str(raw_name).strip().upper()
if any(x in name for x in ["CARREFOUR", "كارفور"]): return "Carrefour"
if any(x in name for x in ["OSCAR", "اوسكار", "GRAND STORES"]): return "Oscar"
if any(x in name for x in ["SEOUDI", "سعودي"]): return "Seoudi"
if any(x in name for x in ["SPINNEY", "SPINEY", "سبينيز"]): return "Spinneys"
if any(x in name for x in ["METRO", "مترو"]): return "Metro Markets"
if any(x in name for x in ["KHEIR ZAMAN", "خير زمان"]): return "Kheir Zaman"
if any(x in name for x in ["ABU ZE KRY", "ABOU ZE KRY", "أبو زكري"]): return "Abu Zekry"
if "LOCAL SHOP" in name: return "Local Shop"
return name.title()
gdf_shops['normalised_name'] = gdf_shops['raw_name'].apply(get_canonical_name)
# 2. Normalize Anchor Names
gdf_anchors['anchor_name_normalised'] = gdf_anchors['anchor_name'].str.strip()
# Prepare for KNN
# Coordinates are in Egypt 1907 / Red Belt (EPSG:22992), which is metric.
shop_coords = np.array(list(zip(gdf_shops.geometry.x, gdf_shops.geometry.y)))
shop_tree = cKDTree(shop_coords)
anchor_coords = np.array(list(zip(gdf_anchors.geometry.x, gdf_anchors.geometry.y)))
anchor_tree = cKDTree(anchor_coords)
results = []
for i, anchor in gdf_anchors.iterrows():
a_id = str(anchor['anchor_id'])
a_name = str(anchor['anchor_name_normalised'])
a_geom = anchor.geometry
a_coord = (anchor.geometry.x, anchor.geometry.y)
# Find 5 nearest shops
# k=5 because we want exactly 5.
distances, indices = shop_tree.query(a_coord, k=5)
# If there are fewer than 5 shops total (unlikely here), handle it.
# But the prompt implies 5.
knn_list = []
# distances and indices are arrays. If k=1, they might be scalars.
if isinstance(distances, float):
distances = [distances]
indices = [indices]
# Re-order by distance if not already (cKDTree query usually returns sorted)
# We'll sort just in case.
sort_idx = np.argsort(distances)
distances = distances[sort_idx]
indices = indices[sort_idx]
for d, idx in zip(distances, indices):
shop = gdf_shops.iloc[idx]
knn_list.append({
"shop_id": str(shop['shop_id']),
"normalised_name": str(shop['normalised_name']),
"distance_m": float(d),
"within_1km": bool(d <= 1000.0)
})
# Find 3 nearest sibling anchors
# A "sibling" anchor is any other anchor.
# The instruction says "closest sibling anchors". Usually this excludes self.
# cKDTree.query with k=4 to skip self (the closest is the anchor itself).
d_anc, i_anc = anchor_tree.query(a_coord, k=4)
# i_anc[0] is the anchor itself.
# We want the next 3.
if isinstance(i_anc, (int, float)): # should not happen for k=4
sibling_indices = []
else:
sibling_indices = i_anc[1:4]
sibling_distances = d_anc[1:4]
# We need a 5x3 distance matrix from these 5 shops to the 3 sibling anchors.
# rows: 5 shops (knn order), columns: 3 sibling anchors.
# matrix[i][j] = distance from shop i to sibling anchor j.
matrix = []
for shop_entry in knn_list:
row = []
s_geom = gdf_shops.iloc[indices[knn_list.index(shop_entry)]].geometry # This is wrong.
# Let's fix the indexing.
pass
# Let's redo the matrix part properly.
matrix = []
for idx_in_knn in range(5):
actual_shop_idx = indices[idx_in_knn]
s_coord = shop_coords[actual_shop_idx]
row = []
for s_anc_idx in sibling_indices:
# Distance between shop and sibling anchor
# We can use the coordinates directly.
anc_coord = anchor_coords[s_anc_idx]
dist_s_a = np.sqrt(np.sum((s_coord - anc_coord)**2))
row.append(float(dist_s_a))
matrix.append(row)
results.append({
"anchor_id": a_id,
"anchor_name_normalised": a_name,
"knn": knn_list,
"full_distance_matrix_m": matrix
})
with open("/work/market_neighbourhoods.json", "w") as f:
json.dump(results, f, indent=2)
if __name__ == "__main__":
solve()
Wrote /work/solve.py (7273 chars).
0:47
Edit
/work/solve.py
old_string not found in /work/solve.py
0:50
Read
/work/solve.py
1 import geopandas as gpd
2 import pandas as pd
3 import json
4 from shapely.ops import nearest_points
5 from scipy.spatial import cKDTree
6 import numpy as np
7
8 def normalize_name(name):
9 if not name or pd.isna(name):
10 return ""
11 name = str(name).strip().upper()
12
13 # Mapping common variations to a canonical name
14 # Based on the ogrinfo output, we see some patterns.
15 # Since we don't have the full list, we'll use a more robust approach.
16 # We'll group by some kind of "semantic" similarity or just a mapping.
17 # For this task, let's define a mapping for the obvious ones.
18
19 mapping = {
20 "CARREFOUR": "Carrefour",
21 "CARREFOUR EGYPT": "Carrefour",
22 "كارفور": "Carrefour",
23 "OSCAR": "Oscar",
24 "اوسكار": "Oscar",
25 "OSCAR GRAND STORES": "Oscar",
26 "SEOUDI MARKET": "Seoudi",
27 "SEOUDI": "Seoudi",
28 "سعودي": "Seoudi",
29 "SEOUDI SUPERMARKET": "Seoudi",
30 "SPINNEYS": "Spinneys",
31 "SPINEYS": "Spinneys",
32 "سبينيز": "Spinneys",
33 "METRO MARKETS": "Metro Markets",
34 "METRO MARKET": "Metro Markets",
35 "مترو": "Metro Markets",
36 "KHEIR ZAMAN": "Kheir Zaman",
37 "خير زمان": "Kheir Zaman"
38 }
39
40 # Check if the name is in our mapping (exact match after stripping/uppercasing)
41 if name in mapping:
42 return mapping[name]
43
44 # For names like "Local Shop 00002", let's just normalize to "Local Shop" or something?
45 # Actually, the instruction says: "the same shop_id always carries the same normalised_name,
46 # and all shops belonging to one chain share a single canonical spelling."
47 # And "Tidy the anchor names too."
48
49 # If it looks like "Local Shop XXXX", let's treat it as "Local Shop"
50 import re
51 if re.match(r"LOCAL SHOP \d+", name):
52 return "Local Shop"
53
54 # For Abu Zekry / Abou Zekry / أبو زكري
55 if "ABU ZE KRY" in name or "ABOU ZE KRY" in name or "أبو زكري" in name:
56 return "Abu Zekry"
57
58 return name
59
60 def solve():
61 # Load data
62 gdf_shops = gpd.read_file("/work/cairo_retail.gpkg", layer="shops")
63 gdf_anchors = gpd.read_file("/work/cairo_retail.gpkg", layer="anchors")
64
65 # 1. Normalize Shop Names
66 # We need to ensure that all shops in a chain share a canonical spelling.
67 # A good way is to create a dictionary: raw_name -> normalized_name.
68 # But we must also ensure that the same normalized_name is used for all shops.
69
70 # First, create a list of all raw names and try to group them.
71 # Given the prompt's specific mention of inconsistent spellings, we'll
72 # use a manual mapping for known chains and a fallback.
73
74 def get_canonical_name(raw_name):
75 name = str(raw_name).strip().upper()
76 if any(x in name for x in ["CARREFOUR", "كارفور"]): return "Carrefour"
77 if any(x in name for x in ["OSCAR", "اوسكار", "GRAND STORES"]): return "Oscar"
78 if any(x in name for x in ["SEOUDI", "سعودي"]): return "Seoudi"
79 if any(x in name for x in ["SPINNEY", "SPINEY", "سبينيز"]): return "Spinneys"
80 if any(x in name for x in ["METRO", "مترو"]): return "Metro Markets"
81 if any(x in name for x in ["KHEIR ZAMAN", "خير زمان"]): return "Kheir Zaman"
82 if any(x in name for x in ["ABU ZE KRY", "ABOU ZE KRY", "أبو زكري"]): return "Abu Zekry"
83 if "LOCAL SHOP" in name: return "Local Shop"
84 return name.title()
85
86 gdf_shops['normalised_name'] = gdf_shops['raw_name'].apply(get_canonical_name)
87
88 # 2. Normalize Anchor Names
89 gdf_anchors['anchor_name_normalised'] = gdf_anchors['anchor_name'].str.strip()
90
91 # Prepare for KNN
92 # Coordinates are in Egypt 1907 / Red Belt (EPSG:22992), which is metric.
93 shop_coords = np.array(list(zip(gdf_shops.geometry.x, gdf_shops.geometry.y)))
94 shop_tree = cKDTree(shop_coords)
95
96 anchor_coords = np.array(list(zip(gdf_anchors.geometry.x, gdf_anchors.geometry.y)))
97 anchor_tree = cKDTree(anchor_coords)
98
99 results = []
100
101 for i, anchor in gdf_anchors.iterrows():
102 a_id = str(anchor['anchor_id'])
103 a_name = str(anchor['anchor_name_normalised'])
104 a_geom = anchor.geometry
105 a_coord = (anchor.geometry.x, anchor.geometry.y)
106
107 # Find 5 nearest shops
108 # k=5 because we want exactly 5.
109 distances, indices = shop_tree.query(a_coord, k=5)
110
111 # If there are fewer than 5 shops total (unlikely here), handle it.
112 # But the prompt implies 5.
113
114 knn_list = []
115 # distances and indices are arrays. If k=1, they might be scalars.
116 if isinstance(distances, float):
117 distances = [distances]
118 indices = [indices]
119
120 # Re-order by distance if not already (cKDTree query usually returns sorted)
121 # We'll sort just in case.
122 sort_idx = np.argsort(distances)
123 distances = distances[sort_idx]
124 indices = indices[sort_idx]
125
126 for d, idx in zip(distances, indices):
127 shop = gdf_shops.iloc[idx]
128 knn_list.append({
129 "shop_id": str(shop['shop_id']),
130 "normalised_name": str(shop['normalised_name']),
131 "distance_m": float(d),
132 "within_1km": bool(d <= 1000.0)
133 })
134
135 # Find 3 nearest sibling anchors
136 # A "sibling" anchor is any other anchor.
137 # The instruction says "closest sibling anchors". Usually this excludes self.
138 # cKDTree.query with k=4 to skip self (the closest is the anchor itself).
139 d_anc, i_anc = anchor_tree.query(a_coord, k=4)
140
141 # i_anc[0] is the anchor itself.
142 # We want the next 3.
143 if isinstance(i_anc, (int, float)): # should not happen for k=4
144 sibling_indices = []
145 else:
146 sibling_indices = i_anc[1:4]
147 sibling_distances = d_anc[1:4]
148
149 # We need a 5x3 distance matrix from these 5 shops to the 3 sibling anchors.
150 # rows: 5 shops (knn order), columns: 3 sibling anchors.
151 # matrix[i][j] = distance from shop i to sibling anchor j.
152
153 matrix = []
154 for shop_entry in knn_list:
155 row = []
156 s_geom = gdf_shops.iloc[indices[knn_list.index(shop_entry)]].geometry # This is wrong.
157 # Let's fix the indexing.
158 pass
159
160 # Let's redo the matrix part properly.
161 matrix = []
162 for idx_in_knn in range(5):
163 actual_shop_idx = indices[idx_in_knn]
164 s_coord = shop_coords[actual_shop_idx]
165
166 row = []
167 for s_anc_idx in sibling_indices:
168 # Distance between shop and sibling anchor
169 # We can use the coordinates directly.
170 anc_coord = anchor_coords[s_anc_idx]
171 dist_s_a = np.sqrt(np.sum((s_coord - anc_coord)**2))
172 row.append(float(dist_s_a))
173 matrix.append(row)
174
175 results.append({
176 "anchor_id": a_id,
177 "anchor_name_normalised": a_name,
178 "knn": knn_list,
179 "full_distance_matrix_m": matrix
180 })
181
182 with open("/work/market_neighbourhoods.json", "w") as f:
183 json.dump(results, f, indent=2)
184
185 if __name__ == "__main__":
186 solve()
1:08
Write
/work/solve.py
content · 92 lines · py
import geopandas as gpd
import pandas as pd
import json
from scipy.spatial import cKDTree
import numpy as np
def get_canonical_name(raw_name):
name = str(raw_name).strip().upper()
if any(x in name for x in ["CARREFOUR", "كارفور"]): return "Carrefour"
if any(x in name for x in ["OSCAR", "اوسكار", "GRAND STORES"]): return "Oscar"
if any(x in name for x in ["SEOUDI", "سعودي"]): return "Seoudi"
if any(x in name for x in ["SPINNEY", "SPINEY", "سبينيز"]): return "Spinneys"
if any(x in name for x in ["METRO", "مترو"]): return "Metro Markets"
if any(x in name for x in ["KHEIR ZAMAN", "خير زمان"]): return "Kheir Zaman"
if any(x in name for x in ["ABU ZE KRY", "ABOU ZE KRY", "أبو زكري"]): return "Abu Zekry"
if "LOCAL SHOP" in name: return "Local Shop"
return name.title()
def solve():
# Load data
gdf_shops = gpd.read_file("/work/cairo_retail.gpkg", layer="shops")
gdf_anchors = gpd.read_file("/work/cairo_retail.gpkg", layer="anchors")
# 1. Normalize Shop Names
gdf_shops['normalised_name'] = gdf_shops['raw_name'].apply(get_canonical_name)
# 2. Normalize Anchor Names
gdf_anchors['anchor_name_normalised'] = gdf_anchors['anchor_name'].str.strip()
# Prepare for KNN
# Coordinates are in Egypt 1907 / Red Belt (EPSG:22992), which is metric.
shop_coords = np.array(list(zip(gdf_shops.geometry.x, gdf_shops.geometry.y)))
shop_tree = cKDTree(shop_coords)
anchor_coords = np.array(list(zip(gdf_anchors.geometry.x, gdf_anchors.geometry.y)))
anchor_tree = cKDTree(anchor_coords)
results = []
for i, anchor in gdf_anchors.iterrows():
a_id = str(anchor['anchor_id'])
a_name = str(anchor['anchor_name_normalised'])
a_coord = (anchor.geometry.x, anchor.geometry.y)
# Find 5 nearest shops
distances, indices = shop_tree.query(a_coord, k=5)
# cKDTree.query with k=5 returns sorted distances
knn_list = []
for d, idx in zip(distances, indices):
shop = gdf_shops.iloc[idx]
knn_list.append({
"shop_id": str(shop['shop_id']),
"normalised_name": str(shop['normalised_name']),
"distance_m": float(d),
"within_1km": bool(d <= 1000.0)
})
# Find 3 nearest sibling anchors (excluding self)
# We need k=4 to skip the first one (itself)
d_anc, i_anc = anchor_tree.query(a_coord, k=4)
# The closest one is index i, skip it.
# Use i_anc[1:4] for the 3 nearest siblings.
sibling_indices = i_anc[1:4]
# 5x3 distance matrix: rows=5 shops, cols=3 sibling anchors
matrix = []
for idx_in_knn in range(5):
actual_shop_idx = indices[idx_in_knn]
s_coord = shop_coords[actual_shop_idx]
row = []
for s_anc_idx in sibling_indices:
anc_coord = anchor_coords[s_anc_idx]
dist_s_a = np.sqrt(np.sum((s_coord - anc_coord)**2))
row.append(float(dist_s_a))
matrix.append(row)
results.append({
"anchor_id": a_id,
"anchor_name_normalised": a_name,
"knn": knn_list,
"full_distance_matrix_m": matrix
})
with open("/work/market_neighbourhoods.json", "w") as f:
json.dump(results, f, indent=2)
if __name__ == "__main__":
solve()
Wrote /work/solve.py (3454 chars).
1:11
Bash
python solve.py
Traceback (most recent call last):
File "/work/solve.py", line 4, in <module>
from scipy.spatial import cKDTree
ModuleNotFoundError: No module named 'scipy'
1:16
Bash
pip install --user scipy
Collecting scipy Downloading scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB) Requirement already satisfied: numpy<2.7,>=1.26.4 in /usr/local/lib/python3.14/site-packages (from scipy) (2.4.6) Downloading scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.2 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.2/35.2 MB 59.8 MB/s 0:00:00 Installing collected packages: scipy Successfully installed scipy-1.17.1
1:26
Bash
python solve.py
(no output)
1:32
Bash
ls -F /work/market_neighbourhoods.json
/work/market_neighbourhoods.json
1:45
Bash
head -n 50 /work/market_neighbourhoods.json
[
{
"anchor_id": "M001",
"anchor_name_normalised": "TAHRIR SQUARE PLAZA",
"knn": [
{
"shop_id": "S07233",
"normalised_name": "Seoudi",
"distance_m": 565.5774729861073,
"within_1km": true
},
{
"shop_id": "S05762",
"normalised_name": "Oscar",
"distance_m": 722.3301477157331,
"within_1km": true
},
{
"shop_id": "S08811",
"normalised_name": "Seoudi",
"distance_m": 851.1481358488758,
"within_1km": true
},
{
"shop_id": "S01722",
"normalised_name": "Local Shop",
"distance_m": 852.4416831354031,
"within_1km": true
},
{
"shop_id": "S04111",
"normalised_name": "Local Shop",
"distance_m": 868.96252213481,
"within_1km": true
}
],
"full_distance_matrix_m": [
[
2487.0078080337594,
2174.324012747061,
3254.5114413442143
],
[
1534.4843964513216,
1756.6458865635916,
2217.728977070344
],
[
1083.478376599746,
2432.536076922434,
1:48
Bash
# Checking the size of the JSON to ensure it's not empty and has reasonable cont...
# Checking the size of the JSON to ensure it's not empty and has reasonable content. wc -l /work/market_neighbourhoods.json
6301 /work/market_neighbourhoods.json
2:01
Bash
python -c "import json; data = json.load(open('/work/market_neighbourhoods.json'...
File "<string>", line 1
import json; data = json.load(open('/work/market_neighbourhoods.json')); print(f'Anchors: {len(data)}'); print(f'First anchor knn length: {len(data[0["knn"]])}'); print(f'First anchor matrix shape: {len(data[0["full_distance_matrix_m"])}, {len(data[0["full_distance_matrix_m"][0]])}')
^
SyntaxError: closing parenthesis ')' does not match opening parenthesis '['
2:09
Bash
python -c "import json; data = json.load(open('/work/market_neighbourhoods.json'...
Anchors: 100 First anchor knn length: 5 First anchor matrix shape: 5, 3
2:28