|
1 | | --- Similar logic to zoning districts: |
2 | | --- calculate how much (total area and percentage) of each lot is covered by MIH areas |
3 | | --- assign the MIH project ID to each tax lot based on which MIH area covers the |
4 | | --- majority of the lot |
5 | | --- a MIH area is only assigned if more than 10% of the lot is covered by the MIH area |
6 | | --- OR more than 50% of the MIH area overlaps with the lot |
7 | | -DROP TABLE IF EXISTS mihperorder; |
8 | | -CREATE TABLE mihperorder AS |
9 | | -WITH mih_unioned AS ( |
10 | | - SELECT |
11 | | - project_name, |
12 | | - mih_option AS affordability_option, |
13 | | - ST_UNION(wkb_geometry) AS wkb_geometry |
14 | | - FROM dcp_mih |
15 | | - GROUP BY project_name, mih_option |
16 | | -), |
17 | | -mihper AS ( |
| 1 | +-- Mandatory Inclusionary Housing (MIH) Area Assignment Logic |
| 2 | +-- |
| 3 | +-- Assign MIH affordability options to tax lots based on spatial overlap with MIH areas |
| 4 | +-- |
| 5 | +-- Assignment Strategy: |
| 6 | +-- Unlike transit zones where each lot gets assigned to exactly one zone, MIH areas can have |
| 7 | +-- multiple overlapping affordability options that ALL apply to a single lot. A lot is assigned |
| 8 | +-- to a MIH option if either: |
| 9 | +-- 1. ≥10% of the lot area is covered by the MIH area, OR |
| 10 | +-- 2. ≥50% of the MIH area is covered by the lot |
| 11 | +-- |
| 12 | +-- Multiple Options Per Lot: |
| 13 | +-- A single lot can legitimately have multiple MIH options (e.g., Option 1, Option 2, Deep Affordability). |
| 14 | +-- These are not competing assignments but rather cumulative policy options that apply to development |
| 15 | +-- on that lot. The final output pivots these into binary flags (mih_opt1, mih_opt2, etc.). |
| 16 | +-- |
| 17 | +-- Data Flow: |
| 18 | +-- 1. Clean MIH option names and create unique identifiers (mih_cleaned table) |
| 19 | +-- 2. Calculate spatial overlaps between lots and MIH areas (mih_lot_overlap table) |
| 20 | +-- 3. Filter to assignments meeting the coverage thresholds |
| 21 | +-- 4. Pivot multiple options per lot into binary columns on the pluto table |
| 22 | + |
| 23 | + |
| 24 | +DROP TABLE IF EXISTS mih_cleaned; |
| 25 | +CREATE TABLE mih_cleaned AS |
| 26 | +SELECT |
| 27 | + project_id || '-' || mih_option AS mih_id, |
| 28 | + *, |
| 29 | + trim( |
| 30 | + -- Step 2b: collapse any sequence of commas (e.g., ",,", ",,,") |
| 31 | + regexp_replace( |
| 32 | + -- Step 2a: Replace "and" or "," (with any spaces) with a single comma |
| 33 | + regexp_replace( |
| 34 | + -- Step 1: Add space between "Option" and number |
| 35 | + regexp_replace( |
| 36 | + replace(mih_option, 'Affordablility', 'Affordability'), -- should probably fix this in the source data |
| 37 | + 'Option(\d)', -- ← match "Option" followed by a digit |
| 38 | + 'Option \1', -- ← insert space |
| 39 | + 'g' |
| 40 | + ), |
| 41 | + '\s*(,|and)\s*', -- ← match a comma or "and" (with spaces) |
| 42 | + ',', -- ← replace with a comma |
| 43 | + 'g' |
| 44 | + ), |
| 45 | + ',+', -- ← match one or more commas in a row |
| 46 | + ',', -- ← replace with a single comma |
| 47 | + 'g' |
| 48 | + ), |
| 49 | + ', ' -- ← trim comma and space FROM start/end |
| 50 | + ) AS cleaned_option |
| 51 | +FROM dcp_mih; |
| 52 | + |
| 53 | + |
| 54 | +DROP TABLE IF EXISTS mih_lot_overlap CASCADE; |
| 55 | +CREATE TABLE mih_lot_overlap AS |
| 56 | +WITH mih_per_area AS ( |
18 | 57 | SELECT |
19 | | - p.id, |
20 | 58 | p.bbl, |
21 | | - m.project_name, |
22 | | - m.affordability_option, |
23 | | - ST_AREA( |
| 59 | + m.project_id, |
| 60 | + m.mih_id, |
| 61 | + m.wkb_geometry AS mih_geom, |
| 62 | + p.geom AS lot_geom, |
| 63 | + m.cleaned_option, |
| 64 | + st_area( |
24 | 65 | CASE |
25 | | - WHEN ST_COVEREDBY(p.geom, m.wkb_geometry) THEN p.geom |
26 | | - ELSE ST_MULTI(ST_INTERSECTION(p.geom, m.wkb_geometry)) |
| 66 | + WHEN st_coveredby(p.geom, m.wkb_geometry) THEN p.geom |
| 67 | + ELSE st_multi(st_intersection(p.geom, m.wkb_geometry)) |
27 | 68 | END |
28 | 69 | ) AS segbblgeom, |
29 | | - ST_AREA(p.geom) AS allbblgeom, |
30 | | - ST_AREA( |
| 70 | + st_area(p.geom) AS allbblgeom, |
| 71 | + st_area( |
31 | 72 | CASE |
32 | | - WHEN ST_COVEREDBY(m.wkb_geometry, p.geom) THEN m.wkb_geometry |
33 | | - ELSE ST_MULTI(ST_INTERSECTION(m.wkb_geometry, p.geom)) |
| 73 | + WHEN st_coveredby(m.wkb_geometry, p.geom) THEN m.wkb_geometry |
| 74 | + ELSE st_multi(st_intersection(m.wkb_geometry, p.geom)) |
34 | 75 | END |
35 | 76 | ) AS segmihgeom, |
36 | | - ST_AREA(m.wkb_geometry) AS allmihgeom |
| 77 | + st_area(m.wkb_geometry) AS allmihgeom |
37 | 78 | FROM pluto AS p |
38 | | - INNER JOIN mih_unioned AS m |
39 | | - ON ST_INTERSECTS(p.geom, m.wkb_geometry) |
| 79 | + INNER JOIN mih_cleaned AS m |
| 80 | + ON st_intersects(p.geom, m.wkb_geometry) |
40 | 81 | ), |
41 | | -grouped AS ( |
| 82 | +mih_areas AS ( |
42 | 83 | SELECT |
43 | | - id, |
44 | 84 | bbl, |
45 | | - project_name, |
46 | | - affordability_option, |
47 | | - SUM(segbblgeom) AS segbblgeom, |
48 | | - SUM(segmihgeom) AS segmihgeom, |
49 | | - SUM(segbblgeom / allbblgeom) * 100 AS perbblgeom, |
50 | | - MAX(segmihgeom / allmihgeom) * 100 AS maxpermihgeom |
51 | | - FROM mihper |
52 | | - GROUP BY id, bbl, project_name, affordability_option |
| 85 | + cleaned_option, |
| 86 | + project_id, |
| 87 | + mih_id, |
| 88 | + sum(segbblgeom) AS segbblgeom, |
| 89 | + sum(segmihgeom) AS segmihgeom, |
| 90 | + sum(segbblgeom / allbblgeom) * 100 AS perbblgeom, |
| 91 | + max(segmihgeom / allmihgeom) * 100 AS maxpermihgeom |
| 92 | + FROM mih_per_area |
| 93 | + GROUP BY bbl, cleaned_option, project_id, mih_id |
53 | 94 | ) |
54 | | -SELECT |
55 | | - id, |
56 | | - bbl, |
57 | | - project_name, |
58 | | - affordability_option, |
59 | | - segbblgeom, |
60 | | - perbblgeom, |
61 | | - maxpermihgeom, |
62 | | - ROW_NUMBER() OVER ( |
63 | | - PARTITION BY id |
64 | | - ORDER BY segbblgeom DESC, segmihgeom DESC |
65 | | - ) AS row_number |
66 | | -FROM grouped |
| 95 | +SELECT * FROM mih_areas |
67 | 96 | WHERE perbblgeom >= 10 OR maxpermihgeom >= 50; |
68 | 97 |
|
69 | | --- assign the MIH project name and affordability option with the highest overlap to each lot |
70 | | -UPDATE pluto a |
| 98 | + |
| 99 | +-- NOTE: GIS will likely refactor dcp_mih into this pivoted format, |
| 100 | +-- so much this code will likely disappear. |
| 101 | +-- |
| 102 | +-- Find all distinct MIH areas that apply to a lot, and pivot to columns. |
| 103 | +-- e.g. if we have two rows from our geospatial join like so: |
| 104 | +-- bbl=123, mih_options=Option 1,Option 2 |
| 105 | +-- bbl=123, mih_options=Option 2,Option 3 |
| 106 | +-- we first aggregate to |
| 107 | +-- bbl=123, Option 1,Option 2,Option 2,Option 3 |
| 108 | +-- then pivot into distinct columns |
| 109 | +WITH bbls_with_all_options AS ( |
| 110 | + SELECT |
| 111 | + bbl, |
| 112 | + string_agg(cleaned_option, ',') AS all_options |
| 113 | + FROM mih_lot_overlap |
| 114 | + GROUP BY bbl |
| 115 | +), pivoted AS ( |
| 116 | + SELECT |
| 117 | + bbl, |
| 118 | + CASE |
| 119 | + WHEN (all_options LIKE '%Option 1%') = true THEN '1' |
| 120 | + END AS mih_opt1, |
| 121 | + CASE |
| 122 | + WHEN (all_options LIKE '%Option 2%') = true THEN '1' |
| 123 | + END AS mih_opt2, |
| 124 | + CASE |
| 125 | + WHEN (all_options LIKE '%Option 3%' OR all_options LIKE '%Deep Affordability Option%') = true THEN '1' |
| 126 | + END AS mih_opt3, |
| 127 | + CASE |
| 128 | + WHEN (all_options LIKE '%Deep Affordability Option%') = true THEN '1' |
| 129 | + END AS mih_opt4 |
| 130 | + FROM bbls_with_all_options |
| 131 | +) |
| 132 | +UPDATE pluto |
71 | 133 | SET |
72 | | - mih_project_name = b.project_name, |
73 | | - mih_affordability_option = b.affordability_option |
74 | | -FROM mihperorder AS b |
75 | | -WHERE |
76 | | - a.id = b.id |
77 | | - AND row_number = 1; |
| 134 | + mih_opt1 = m.mih_opt1, |
| 135 | + mih_opt2 = m.mih_opt2, |
| 136 | + mih_opt3 = m.mih_opt3, |
| 137 | + mih_opt4 = m.mih_opt4 |
| 138 | +FROM pivoted AS m |
| 139 | +WHERE pluto.bbl = m.bbl |
0 commit comments