[{"data":1,"prerenderedAt":1773},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis":3},{"id":4,"title":5,"body":6,"description":1762,"extension":1763,"meta":1764,"navigation":292,"path":1769,"seo":1770,"stem":1771,"__hash__":1772},"docs\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis\u002Findex.md","Test Whether Two Geometries Intersect in PyQGIS",{"type":7,"value":8,"toc":1749},"minimark",[9,13,21,45,216,221,242,246,260,434,447,450,515,527,531,536,640,731,742,746,749,891,975,995,999,1010,1136,1160,1164,1173,1287,1496,1517,1521,1527,1576,1582,1586,1648,1652,1657,1661,1673,1686,1692,1705,1718,1722,1745],[10,11,5],"h1",{"id":12},"test-whether-two-geometries-intersect-in-pyqgis",[14,15,16,20],"p",{},[17,18,19],"code",{},"intersects()"," returns a boolean and looks like the simplest method in the geometry API. It is also the one most often used where a different predicate was meant, because its definition is broader than the everyday word suggests: two shapes that merely share a boundary point intersect, even though their interiors never meet. Spatial joins that double-count along shared edges almost always trace back to that single fact.",[14,22,23,24,29,30,33,34,33,37,40,41,44],{},"This page is a focused recipe within ",[25,26,28],"a",{"href":27},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002F","Geometry Operations and Spatial Predicates in PyQGIS",". It covers the basic test, the boundary cases that make it surprising, how to choose between ",[17,31,32],{},"intersects",", ",[17,35,36],{},"contains",[17,38,39],{},"within"," and ",[17,42,43],{},"overlaps",", and how to make the test fast over a whole layer.",[14,46,47],{},[48,49,54,58,62,69,78,117,144,168,186,211],"svg",{"viewBox":50,"role":51,"ariaLabel":52,"xmlns":53},"0 0 760 262","img","Five arrangements of two shapes with the result of intersects for each, showing that a shared boundary point and full containment both count as intersecting","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[55,56,57],"title",{},"When intersects() returns true",[59,60,61],"desc",{},"Five small panels each show a blue shape and a teal shape in a different arrangement: fully apart, corner touching, edge touching, partially overlapping and one inside the other. Beneath each is the boolean result of intersects, which is false only for the fully separated pair.",[63,64],"rect",{"x":65,"y":65,"width":66,"height":67,"fill":68},"0","760","262","#f6f3ea",[70,71,77],"text",{"x":72,"y":73,"style":74,"fill":75,"textAnchor":76},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Only complete separation returns false",[79,80,81,91,98,106,111],"g",{},[63,82],{"x":83,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"12","44","140","150","10","#fffdf7","#59645f","stroke-width:1.5",[70,92,97],{"x":93,"y":94,"style":95,"fill":96,"textAnchor":76},"82","68","text-anchor:middle;font-size:11.5px;font-family:sans-serif","#2f3b35","apart",[63,99],{"x":100,"y":101,"width":102,"height":102,"fill":103,"fillOpacity":104,"stroke":103,"style":105},"28","92","42","#2563eb",0.25,"stroke-width:2",[63,107],{"x":108,"y":109,"width":102,"height":102,"fill":110,"fillOpacity":104,"stroke":110,"style":105},"94","126","#0f766e",[70,112,116],{"x":93,"y":113,"style":114,"fill":115,"textAnchor":76},"222","text-anchor:middle;font-size:15px;font-weight:bold;font-family:sans-serif","#b91c1c","False",[79,118,119,122,126,131,135,140],{},[63,120],{"x":121,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"164",[70,123,125],{"x":124,"y":94,"style":95,"fill":96,"textAnchor":76},"234","corner touch",[63,127],{"x":128,"y":129,"width":130,"height":130,"fill":103,"fillOpacity":104,"stroke":103,"style":105},"180","88","46",[63,132],{"x":133,"y":134,"width":130,"height":130,"fill":110,"fillOpacity":104,"stroke":110,"style":105},"226","134",[136,137],"circle",{"cx":133,"cy":134,"r":138,"fill":139},"5","#b45309",[70,141,143],{"x":124,"y":113,"style":114,"fill":142,"textAnchor":76},"#15803d","True",[79,145,146,149,153,159,161,166],{},[63,147],{"x":148,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"316",[70,150,152],{"x":151,"y":94,"style":95,"fill":96,"textAnchor":76},"386","shared edge",[63,154],{"x":155,"y":156,"width":157,"height":158,"fill":103,"fillOpacity":104,"stroke":103,"style":105},"332","102","54","60",[63,160],{"x":151,"y":156,"width":157,"height":158,"fill":110,"fillOpacity":104,"stroke":110,"style":105},[162,163],"line",{"x1":151,"y1":156,"x2":151,"y2":164,"stroke":139,"style":165},"162","stroke-width:3",[70,167,143],{"x":151,"y":113,"style":114,"fill":142,"textAnchor":76},[79,169,170,173,177,181,184],{},[63,171],{"x":172,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"468",[70,174,176],{"x":175,"y":94,"style":95,"fill":96,"textAnchor":76},"538","overlapping",[63,178],{"x":179,"y":156,"width":180,"height":158,"fill":103,"fillOpacity":104,"stroke":103,"style":105},"484","58",[63,182],{"x":183,"y":156,"width":180,"height":158,"fill":110,"fillOpacity":104,"stroke":110,"style":105},"520",[70,185,143],{"x":175,"y":113,"style":114,"fill":142,"textAnchor":76},[79,187,188,192,196,202,209],{},[63,189],{"x":190,"y":84,"width":191,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"620","128",[70,193,195],{"x":194,"y":94,"style":95,"fill":96,"textAnchor":76},"684","contained",[63,197],{"x":198,"y":199,"width":101,"height":200,"fill":110,"fillOpacity":201,"stroke":110,"style":105},"638","96","72",0.2,[63,203],{"x":204,"y":205,"width":206,"height":207,"fill":103,"fillOpacity":208,"stroke":103,"style":105},"664","116","40","32",0.45,[70,210,143],{"x":194,"y":113,"style":114,"fill":142,"textAnchor":76},[70,212,215],{"x":72,"y":213,"style":214,"fill":89,"textAnchor":76},"250","text-anchor:middle;font-size:11px;font-family:sans-serif","the orange marks show the only shared points in the touching cases",[217,218,220],"h2",{"id":219},"prerequisites","Prerequisites",[222,223,224,232,239],"ul",{},[225,226,227,231],"li",{},[228,229,230],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[225,233,234,235,238],{},"Two layers, or two geometries, ",[228,236,237],{},"in the same CRS",". Predicates compare raw coordinates and do not reproject.",[225,240,241],{},"Valid geometry — GEOS predicates can raise on self-intersecting input.",[217,243,245],{"id":244},"the-basic-test","The basic test",[14,247,248,249,252,253,40,256,259],{},"Both geometries must be ",[17,250,251],{},"QgsGeometry"," objects, and the method is symmetric: ",[17,254,255],{},"a.intersects(b)",[17,257,258],{},"b.intersects(a)"," always agree.",[261,262,267],"pre",{"className":263,"code":264,"language":265,"meta":266,"style":266},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nzones = QgsProject.instance().mapLayersByName(\"flood_zones\")[0]\n\nzone_geometry = next(zones.getFeatures()).geometry()\n\nhits = [\n    feature[\"parcel_id\"]\n    for feature in parcels.getFeatures()\n    if feature.geometry().intersects(zone_geometry)\n]\nprint(len(hits), \"parcel(s) touch the flood zone\")\n","python","",[17,268,269,287,294,319,338,343,357,362,373,384,399,408,413],{"__ignoreMap":266},[270,271,273,277,281,284],"span",{"class":162,"line":272},1,[270,274,276],{"class":275},"snl16","from",[270,278,280],{"class":279},"s95oV"," qgis.core ",[270,282,283],{"class":275},"import",[270,285,286],{"class":279}," QgsProject\n",[270,288,290],{"class":162,"line":289},2,[270,291,293],{"emptyLinePlaceholder":292},true,"\n",[270,295,297,300,303,306,310,313,316],{"class":162,"line":296},3,[270,298,299],{"class":279},"parcels ",[270,301,302],{"class":275},"=",[270,304,305],{"class":279}," QgsProject.instance().mapLayersByName(",[270,307,309],{"class":308},"sU2Wk","\"parcels\"",[270,311,312],{"class":279},")[",[270,314,65],{"class":315},"sDLfK",[270,317,318],{"class":279},"]\n",[270,320,322,325,327,329,332,334,336],{"class":162,"line":321},4,[270,323,324],{"class":279},"zones ",[270,326,302],{"class":275},[270,328,305],{"class":279},[270,330,331],{"class":308},"\"flood_zones\"",[270,333,312],{"class":279},[270,335,65],{"class":315},[270,337,318],{"class":279},[270,339,341],{"class":162,"line":340},5,[270,342,293],{"emptyLinePlaceholder":292},[270,344,346,349,351,354],{"class":162,"line":345},6,[270,347,348],{"class":279},"zone_geometry ",[270,350,302],{"class":275},[270,352,353],{"class":315}," next",[270,355,356],{"class":279},"(zones.getFeatures()).geometry()\n",[270,358,360],{"class":162,"line":359},7,[270,361,293],{"emptyLinePlaceholder":292},[270,363,365,368,370],{"class":162,"line":364},8,[270,366,367],{"class":279},"hits ",[270,369,302],{"class":275},[270,371,372],{"class":279}," [\n",[270,374,376,379,382],{"class":162,"line":375},9,[270,377,378],{"class":279},"    feature[",[270,380,381],{"class":308},"\"parcel_id\"",[270,383,318],{"class":279},[270,385,387,390,393,396],{"class":162,"line":386},10,[270,388,389],{"class":275},"    for",[270,391,392],{"class":279}," feature ",[270,394,395],{"class":275},"in",[270,397,398],{"class":279}," parcels.getFeatures()\n",[270,400,402,405],{"class":162,"line":401},11,[270,403,404],{"class":275},"    if",[270,406,407],{"class":279}," feature.geometry().intersects(zone_geometry)\n",[270,409,411],{"class":162,"line":410},12,[270,412,318],{"class":279},[270,414,416,419,422,425,428,431],{"class":162,"line":415},13,[270,417,418],{"class":315},"print",[270,420,421],{"class":279},"(",[270,423,424],{"class":315},"len",[270,426,427],{"class":279},"(hits), ",[270,429,430],{"class":308},"\"parcel(s) touch the flood zone\"",[270,432,433],{"class":279},")\n",[14,435,436,439,440,443,444,446],{},[228,437,438],{},"Breakdown:"," ",[17,441,442],{},"feature.geometry()"," returns the geometry handle; calling ",[17,445,19],{}," on it does the full GEOS test. A list comprehension is fine here because the predicate is cheap, but note that every feature is read from the provider — the prefiltering section below is what makes this scale.",[14,448,449],{},"If the two layers are in different CRSs the answer will be silently wrong rather than an error, because the coordinate ranges simply never overlap. Guard for it:",[261,451,453],{"className":263,"code":452,"language":265,"meta":266,"style":266},"if parcels.crs() != zones.crs():\n    raise ValueError(\n        f\"CRS mismatch: {parcels.crs().authid()} vs {zones.crs().authid()}\"\n    )\n",[17,454,455,469,480,510],{"__ignoreMap":266},[270,456,457,460,463,466],{"class":162,"line":272},[270,458,459],{"class":275},"if",[270,461,462],{"class":279}," parcels.crs() ",[270,464,465],{"class":275},"!=",[270,467,468],{"class":279}," zones.crs():\n",[270,470,471,474,477],{"class":162,"line":289},[270,472,473],{"class":275},"    raise",[270,475,476],{"class":315}," ValueError",[270,478,479],{"class":279},"(\n",[270,481,482,485,488,491,494,497,500,502,505,507],{"class":162,"line":296},[270,483,484],{"class":275},"        f",[270,486,487],{"class":308},"\"CRS mismatch: ",[270,489,490],{"class":315},"{",[270,492,493],{"class":279},"parcels.crs().authid()",[270,495,496],{"class":315},"}",[270,498,499],{"class":308}," vs ",[270,501,490],{"class":315},[270,503,504],{"class":279},"zones.crs().authid()",[270,506,496],{"class":315},[270,508,509],{"class":308},"\"\n",[270,511,512],{"class":162,"line":321},[270,513,514],{"class":279},"    )\n",[14,516,517,519,520,523,524,526],{},[228,518,438],{}," Comparing ",[17,521,522],{},"QgsCoordinateReferenceSystem"," objects with ",[17,525,465],{}," is reliable. Raising rather than reprojecting silently is the right default in a script someone else will run — a mismatch usually means the wrong layer was picked, not that a transform is needed.",[217,528,530],{"id":529},"choose-the-right-predicate","Choose the right predicate",[14,532,533,535],{},[17,534,19],{}," answers \"do these share any point at all\". Most real questions are narrower, and picking the specific predicate makes the intent readable and the result correct.",[537,538,539,555],"table",{},[540,541,542],"thead",{},[543,544,545,549,552],"tr",{},[546,547,548],"th",{},"Question",[546,550,551],{},"Predicate",[546,553,554],{},"Note",[556,557,558,571,584,599,612,625],"tbody",{},[543,559,560,564,568],{},[561,562,563],"td",{},"Is any part of the parcel in the zone?",[561,565,566],{},[17,567,19],{},[561,569,570],{},"Includes a boundary-only touch",[543,572,573,576,581],{},[561,574,575],{},"Is the parcel entirely in the zone?",[561,577,578],{},[17,579,580],{},"within()",[561,582,583],{},"True for identical geometries too",[543,585,586,589,594],{},[561,587,588],{},"Does the zone completely hold the parcel?",[561,590,591],{},[17,592,593],{},"contains()",[561,595,596,597],{},"The inverse of ",[17,598,580],{},[543,600,601,604,609],{},[561,602,603],{},"Do they share area without either containing the other?",[561,605,606],{},[17,607,608],{},"overlaps()",[561,610,611],{},"Excludes touching and containment",[543,613,614,617,622],{},[561,615,616],{},"Do they meet only at a boundary?",[561,618,619],{},[17,620,621],{},"touches()",[561,623,624],{},"Interiors must not meet",[543,626,627,630,635],{},[561,628,629],{},"Are they completely separate?",[561,631,632],{},[17,633,634],{},"disjoint()",[561,636,637,638],{},"The negation of ",[17,639,19],{},[261,641,643],{"className":263,"code":642,"language":265,"meta":266,"style":266},"for feature in parcels.getFeatures():\n    geometry = feature.geometry()\n    if geometry.within(zone_geometry):\n        category = \"wholly inside\"\n    elif geometry.intersects(zone_geometry):\n        category = \"partly inside\"\n    else:\n        category = \"outside\"\n    print(feature[\"parcel_id\"], category)\n",[17,644,645,657,667,674,684,692,701,709,718],{"__ignoreMap":266},[270,646,647,650,652,654],{"class":162,"line":272},[270,648,649],{"class":275},"for",[270,651,392],{"class":279},[270,653,395],{"class":275},[270,655,656],{"class":279}," parcels.getFeatures():\n",[270,658,659,662,664],{"class":162,"line":289},[270,660,661],{"class":279},"    geometry ",[270,663,302],{"class":275},[270,665,666],{"class":279}," feature.geometry()\n",[270,668,669,671],{"class":162,"line":296},[270,670,404],{"class":275},[270,672,673],{"class":279}," geometry.within(zone_geometry):\n",[270,675,676,679,681],{"class":162,"line":321},[270,677,678],{"class":279},"        category ",[270,680,302],{"class":275},[270,682,683],{"class":308}," \"wholly inside\"\n",[270,685,686,689],{"class":162,"line":340},[270,687,688],{"class":275},"    elif",[270,690,691],{"class":279}," geometry.intersects(zone_geometry):\n",[270,693,694,696,698],{"class":162,"line":345},[270,695,678],{"class":279},[270,697,302],{"class":275},[270,699,700],{"class":308}," \"partly inside\"\n",[270,702,703,706],{"class":162,"line":359},[270,704,705],{"class":275},"    else",[270,707,708],{"class":279},":\n",[270,710,711,713,715],{"class":162,"line":364},[270,712,678],{"class":279},[270,714,302],{"class":275},[270,716,717],{"class":308}," \"outside\"\n",[270,719,720,723,726,728],{"class":162,"line":375},[270,721,722],{"class":315},"    print",[270,724,725],{"class":279},"(feature[",[270,727,381],{"class":308},[270,729,730],{"class":279},"], category)\n",[14,732,733,735,736,738,739,741],{},[228,734,438],{}," Testing ",[17,737,580],{}," first and ",[17,740,19],{}," second encodes the specificity — every geometry that is within another also intersects it, so the reverse order would classify everything as \"partly inside\". This three-way split is the shape most real classification code takes.",[217,743,745],{"id":744},"prefilter-with-bounding-boxes","Prefilter with bounding boxes",[14,747,748],{},"An exact predicate is a real geometric computation. A bounding-box comparison is four number comparisons. Running the cheap test first discards the overwhelming majority of non-matches for almost nothing.",[14,750,751],{},[48,752,755,758,761,763,778,781,785,791,821,827,831,834,838,842,845,848,851,854,865,869,873,876,880,884,888],{"viewBox":753,"role":51,"ariaLabel":754,"xmlns":53},"0 0 760 250","A two-stage test: a bounding box comparison rejects most candidates immediately, and only the survivors receive the expensive exact predicate",[55,756,757],{},"Cheap test first, exact test second",[59,759,760],{},"Twelve candidate features enter a bounding box comparison. Nine are rejected immediately because their boxes do not overlap the query box. Three survive and are passed to the exact intersects test, of which two are confirmed true and one is a false positive rejected at the exact stage.",[63,762],{"x":65,"y":65,"width":66,"height":213,"fill":68},[764,765,766],"defs",{},[767,768,774],"marker",{"id":769,"viewBox":770,"refX":771,"refY":138,"markerWidth":772,"markerHeight":772,"orient":773},"isecStageArrow","0 0 10 10","8","7","auto-start-reverse",[775,776],"path",{"d":777,"fill":96},"M0 0 L10 5 L0 10 z",[70,779,780],{"x":72,"y":73,"style":74,"fill":75,"textAnchor":76},"The box test is an approximation — it never replaces the exact one",[63,782],{"x":783,"y":784,"width":121,"height":121,"rx":87,"fill":88,"stroke":89,"style":105},"16","52",[70,786,790],{"x":787,"y":788,"style":789,"fill":75,"textAnchor":76},"98","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","12 candidates",[79,792,793,798,800,802,804,806,808,810,812,815,817,819],{"fill":89},[136,794],{"cx":795,"cy":796,"r":797},"48","106","6",[136,799],{"cx":93,"cy":796,"r":797},[136,801],{"cx":205,"cy":796,"r":797},[136,803],{"cx":86,"cy":796,"r":797},[136,805],{"cx":795,"cy":85,"r":797},[136,807],{"cx":93,"cy":85,"r":797},[136,809],{"cx":205,"cy":85,"r":797},[136,811],{"cx":86,"cy":85,"r":797},[136,813],{"cx":795,"cy":814,"r":797},"174",[136,816],{"cx":93,"cy":814,"r":797},[136,818],{"cx":205,"cy":814,"r":797},[136,820],{"cx":86,"cy":814,"r":797},[63,822],{"x":823,"y":788,"width":824,"height":205,"rx":87,"fill":825,"stroke":110,"style":826},"232","168","#26322d","stroke-width:2.5",[70,828,830],{"x":148,"y":796,"style":789,"fill":829,"textAnchor":76},"#d9f99d","boundingBox()",[70,832,833],{"x":148,"y":191,"style":95,"fill":829,"textAnchor":76},".intersects(box)",[70,835,837],{"x":148,"y":836,"style":214,"fill":829,"textAnchor":76},"152","4 number comparisons",[70,839,841],{"x":148,"y":840,"style":214,"fill":829,"textAnchor":76},"172","rejects 9",[63,843],{"x":844,"y":788,"width":836,"height":205,"rx":87,"fill":88,"stroke":139,"style":826},"452",[70,846,19],{"x":847,"y":796,"style":789,"fill":139,"textAnchor":76},"528",[70,849,850],{"x":847,"y":191,"style":214,"fill":96,"textAnchor":76},"full GEOS test",[70,852,853],{"x":847,"y":86,"style":214,"fill":96,"textAnchor":76},"3 candidates",[79,855,856,859,862],{},[136,857],{"cx":858,"cy":840,"r":797,"fill":142},"500",[136,860],{"cx":861,"cy":840,"r":797,"fill":142},"522",[136,863],{"cx":864,"cy":840,"r":797,"fill":115},"550",[63,866],{"x":867,"y":199,"width":868,"height":788,"rx":87,"fill":88,"stroke":142,"style":826},"640","104",[70,870,872],{"x":871,"y":109,"style":789,"fill":142,"textAnchor":76},"692","2 real hits",[70,874,875],{"x":871,"y":86,"style":214,"fill":96,"textAnchor":76},"1 was a box-only",[162,877],{"x1":128,"y1":134,"x2":878,"y2":134,"stroke":96,"style":879},"228","stroke-width:2.5;marker-end:url(#isecStageArrow)",[162,881],{"x1":882,"y1":134,"x2":883,"y2":134,"stroke":96,"style":879},"400","448",[162,885],{"x1":886,"y1":134,"x2":887,"y2":134,"stroke":142,"style":879},"604","636",[70,889,890],{"x":72,"y":124,"style":214,"fill":89,"textAnchor":76},"a diagonal line's box can overlap a shape the line never touches",[261,892,894],{"className":263,"code":893,"language":265,"meta":266,"style":266},"from qgis.core import QgsFeatureRequest\n\nzone_box = zone_geometry.boundingBox()\nrequest = QgsFeatureRequest().setFilterRect(zone_box)\n\nhits = [\n    f[\"parcel_id\"]\n    for f in parcels.getFeatures(request)\n    if f.geometry().intersects(zone_geometry)\n]\n",[17,895,896,907,911,921,931,935,943,952,964,971],{"__ignoreMap":266},[270,897,898,900,902,904],{"class":162,"line":272},[270,899,276],{"class":275},[270,901,280],{"class":279},[270,903,283],{"class":275},[270,905,906],{"class":279}," QgsFeatureRequest\n",[270,908,909],{"class":162,"line":289},[270,910,293],{"emptyLinePlaceholder":292},[270,912,913,916,918],{"class":162,"line":296},[270,914,915],{"class":279},"zone_box ",[270,917,302],{"class":275},[270,919,920],{"class":279}," zone_geometry.boundingBox()\n",[270,922,923,926,928],{"class":162,"line":321},[270,924,925],{"class":279},"request ",[270,927,302],{"class":275},[270,929,930],{"class":279}," QgsFeatureRequest().setFilterRect(zone_box)\n",[270,932,933],{"class":162,"line":340},[270,934,293],{"emptyLinePlaceholder":292},[270,936,937,939,941],{"class":162,"line":345},[270,938,367],{"class":279},[270,940,302],{"class":275},[270,942,372],{"class":279},[270,944,945,948,950],{"class":162,"line":359},[270,946,947],{"class":279},"    f[",[270,949,381],{"class":308},[270,951,318],{"class":279},[270,953,954,956,959,961],{"class":162,"line":364},[270,955,389],{"class":275},[270,957,958],{"class":279}," f ",[270,960,395],{"class":275},[270,962,963],{"class":279}," parcels.getFeatures(request)\n",[270,965,966,968],{"class":162,"line":375},[270,967,404],{"class":275},[270,969,970],{"class":279}," f.geometry().intersects(zone_geometry)\n",[270,972,973],{"class":162,"line":386},[270,974,318],{"class":279},[14,976,977,439,979,982,983,985,986,989,990,994],{},[228,978,438],{},[17,980,981],{},"setFilterRect()"," pushes the box test down to the provider, so features outside it are never read at all — a much bigger saving than filtering in Python. The exact ",[17,984,19],{}," still runs on what survives, because a bounding box is a coarse approximation: a long diagonal line has a large box that overlaps shapes the line itself misses entirely. For repeated queries against many geometries, promote this to a ",[17,987,988],{},"QgsSpatialIndex"," — see ",[25,991,993],{"href":992},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuild-spatial-index-pyqgis\u002F","Build and Use a Spatial Index in PyQGIS",".",[217,996,998],{"id":997},"test-many-pairs-efficiently","Test many pairs efficiently",[14,1000,1001,1002,1004,1005,1009],{},"When both sides are layers, prepare the query geometry once. GEOS can build an internal index for a geometry that will be tested repeatedly, and ",[17,1003,251],{}," exposes it through a ",[1006,1007,1008],"em",{},"prepared"," form used automatically when you call the predicate in a tight loop against the same object.",[261,1011,1013],{"className":263,"code":1012,"language":265,"meta":266,"style":266},"from qgis.core import QgsFeatureRequest, QgsGeometry\n\nzone_geometries = [f.geometry() for f in zones.getFeatures()]\nmerged = QgsGeometry.unaryUnion(zone_geometries)\nbox = merged.boundingBox()\n\nrequest = QgsFeatureRequest().setFilterRect(box)\naffected = [\n    f[\"parcel_id\"]\n    for f in parcels.getFeatures(request)\n    if f.geometry().intersects(merged)\n]\nprint(len(affected), \"parcel(s) affected by any zone\")\n",[17,1014,1015,1026,1030,1049,1059,1069,1073,1082,1091,1099,1109,1116,1120],{"__ignoreMap":266},[270,1016,1017,1019,1021,1023],{"class":162,"line":272},[270,1018,276],{"class":275},[270,1020,280],{"class":279},[270,1022,283],{"class":275},[270,1024,1025],{"class":279}," QgsFeatureRequest, QgsGeometry\n",[270,1027,1028],{"class":162,"line":289},[270,1029,293],{"emptyLinePlaceholder":292},[270,1031,1032,1035,1037,1040,1042,1044,1046],{"class":162,"line":296},[270,1033,1034],{"class":279},"zone_geometries ",[270,1036,302],{"class":275},[270,1038,1039],{"class":279}," [f.geometry() ",[270,1041,649],{"class":275},[270,1043,958],{"class":279},[270,1045,395],{"class":275},[270,1047,1048],{"class":279}," zones.getFeatures()]\n",[270,1050,1051,1054,1056],{"class":162,"line":321},[270,1052,1053],{"class":279},"merged ",[270,1055,302],{"class":275},[270,1057,1058],{"class":279}," QgsGeometry.unaryUnion(zone_geometries)\n",[270,1060,1061,1064,1066],{"class":162,"line":340},[270,1062,1063],{"class":279},"box ",[270,1065,302],{"class":275},[270,1067,1068],{"class":279}," merged.boundingBox()\n",[270,1070,1071],{"class":162,"line":345},[270,1072,293],{"emptyLinePlaceholder":292},[270,1074,1075,1077,1079],{"class":162,"line":359},[270,1076,925],{"class":279},[270,1078,302],{"class":275},[270,1080,1081],{"class":279}," QgsFeatureRequest().setFilterRect(box)\n",[270,1083,1084,1087,1089],{"class":162,"line":364},[270,1085,1086],{"class":279},"affected ",[270,1088,302],{"class":275},[270,1090,372],{"class":279},[270,1092,1093,1095,1097],{"class":162,"line":375},[270,1094,947],{"class":279},[270,1096,381],{"class":308},[270,1098,318],{"class":279},[270,1100,1101,1103,1105,1107],{"class":162,"line":386},[270,1102,389],{"class":275},[270,1104,958],{"class":279},[270,1106,395],{"class":275},[270,1108,963],{"class":279},[270,1110,1111,1113],{"class":162,"line":401},[270,1112,404],{"class":275},[270,1114,1115],{"class":279}," f.geometry().intersects(merged)\n",[270,1117,1118],{"class":162,"line":410},[270,1119,318],{"class":279},[270,1121,1122,1124,1126,1128,1131,1134],{"class":162,"line":415},[270,1123,418],{"class":315},[270,1125,421],{"class":279},[270,1127,424],{"class":315},[270,1129,1130],{"class":279},"(affected), ",[270,1132,1133],{"class":308},"\"parcel(s) affected by any zone\"",[270,1135,433],{"class":279},[14,1137,1138,1140,1141,1144,1145,1148,1149,1151,1152,1155,1156,1159],{},[228,1139,438],{}," Unioning the zones once turns ",[1006,1142,1143],{},"n"," × ",[1006,1146,1147],{},"m"," comparisons into ",[1006,1150,1143],{}," × 1. That is almost always the right move when the question is \"does this touch any of them\" rather than \"which one does it touch\". ",[17,1153,1154],{},"unaryUnion()"," merges the whole collection in a single optimised pass rather than pairwise. If you do need to know ",[1006,1157,1158],{},"which"," zone, keep them separate and use a spatial index instead.",[217,1161,1163],{"id":1162},"point-in-polygon-the-special-case","Point in polygon: the special case",[14,1165,1166,1167,1169,1170,1172],{},"Point-in-polygon is the most common predicate question of all, and it has one genuine ambiguity: a point lying exactly on the boundary. ",[17,1168,580],{}," excludes it, ",[17,1171,19],{}," includes it, and which you want depends on whether the polygons tile a surface.",[14,1174,1175],{},[48,1176,1179,1182,1185,1188,1191,1194,1199,1201,1206,1210,1214,1216,1223,1227,1231,1237,1241,1245,1249,1264],{"viewBox":1177,"role":51,"ariaLabel":1178,"xmlns":53},"0 0 760 244","Three points relative to a polygon — inside, exactly on the boundary and outside — with the result of within, intersects and contains for each",[55,1180,1181],{},"How each predicate treats a boundary point",[59,1183,1184],{},"A polygon is drawn with three points: one clearly inside, one sitting exactly on an edge, and one outside. A table beneath records that within is true only for the interior point, intersects is true for both the interior and boundary points, and contains behaves identically to within.",[63,1186],{"x":65,"y":65,"width":66,"height":1187,"fill":68},"244",[70,1189,1190],{"x":72,"y":73,"style":74,"fill":75,"textAnchor":76},"A point on the boundary is the only ambiguous case",[63,1192],{"x":783,"y":84,"width":155,"height":1193,"rx":87,"fill":88,"stroke":110,"style":826},"186",[775,1195],{"d":1196,"fill":110,"fillOpacity":1197,"stroke":110,"style":1198},"M60 200 L60 90 L200 76 L300 128 L272 200 Z",0.14,"stroke-width:2.5;stroke-linejoin:round",[136,1200],{"cx":86,"cy":85,"r":772,"fill":142},[70,1202,1205],{"x":121,"y":1203,"style":1204,"fill":96},"136","font-size:11.5px;font-family:sans-serif","A · inside",[136,1207],{"cx":1208,"cy":1209,"r":772,"fill":139},"130","83",[70,1211,1213],{"x":1212,"y":200,"style":1204,"fill":96},"144","B · on the edge",[136,1215],{"cx":148,"cy":1193,"r":772,"fill":115},[70,1217,1222],{"x":1218,"y":1219,"style":1220,"fill":96,"textAnchor":1221},"308","212","text-anchor:end;font-size:11.5px;font-family:sans-serif","end","C · outside",[63,1224],{"x":1225,"y":84,"width":1226,"height":1193,"rx":87,"fill":88,"stroke":89,"style":105},"384","360",[162,1228],{"x1":1229,"y1":108,"x2":1230,"y2":108,"stroke":89,"style":90},"404","724",[70,1232,1236],{"x":1233,"y":1234,"style":1235,"fill":75,"textAnchor":76},"480","80","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","predicate",[70,1238,1240],{"x":1239,"y":1234,"style":1235,"fill":142,"textAnchor":76},"600","A",[70,1242,1244],{"x":1243,"y":1234,"style":1235,"fill":139,"textAnchor":76},"656","B",[70,1246,1248],{"x":1247,"y":1234,"style":1235,"fill":115,"textAnchor":76},"712","C",[79,1250,1251,1256,1260],{"style":1204},[70,1252,1255],{"x":1253,"y":1254,"fill":96},"410","124","point.within(poly)",[70,1257,1259],{"x":1253,"y":1258,"fill":96},"160","point.intersects(poly)",[70,1261,1263],{"x":1253,"y":1262,"fill":96},"196","poly.contains(point)",[79,1265,1267,1270,1273,1275,1277,1279,1281,1283,1285],{"style":1266},"font-size:14px;font-weight:bold;font-family:sans-serif;text-anchor:middle",[70,1268,1269],{"x":1239,"y":1254,"fill":142},"✓",[70,1271,1272],{"x":1243,"y":1254,"fill":115},"✗",[70,1274,1272],{"x":1247,"y":1254,"fill":115},[70,1276,1269],{"x":1239,"y":1258,"fill":142},[70,1278,1269],{"x":1243,"y":1258,"fill":142},[70,1280,1272],{"x":1247,"y":1258,"fill":115},[70,1282,1269],{"x":1239,"y":1262,"fill":142},[70,1284,1272],{"x":1243,"y":1262,"fill":115},[70,1286,1272],{"x":1247,"y":1262,"fill":115},[261,1288,1290],{"className":263,"code":1289,"language":265,"meta":266,"style":266},"from qgis.core import QgsFeatureRequest, QgsProject, QgsSpatialIndex\n\npoints = QgsProject.instance().mapLayersByName(\"incidents\")[0]\ndistricts = QgsProject.instance().mapLayersByName(\"districts\")[0]\n\nindex = QgsSpatialIndex(\n    districts.getFeatures(), flags=QgsSpatialIndex.FlagStoreFeatureGeometries\n)\nnames = {f.id(): f[\"district_name\"] for f in districts.getFeatures()}\n\nfor point in points.getFeatures():\n    geometry = point.geometry()\n    assigned = None\n    for did in index.intersects(geometry.boundingBox()):\n        if geometry.within(index.geometry(did)):\n            assigned = names[did]\n            break\n    print(point[\"incident_id\"], assigned or \"unassigned\")\n",[17,1291,1292,1303,1307,1325,1343,1347,1357,1371,1375,1400,1404,1416,1425,1435,1448,1457,1468,1474],{"__ignoreMap":266},[270,1293,1294,1296,1298,1300],{"class":162,"line":272},[270,1295,276],{"class":275},[270,1297,280],{"class":279},[270,1299,283],{"class":275},[270,1301,1302],{"class":279}," QgsFeatureRequest, QgsProject, QgsSpatialIndex\n",[270,1304,1305],{"class":162,"line":289},[270,1306,293],{"emptyLinePlaceholder":292},[270,1308,1309,1312,1314,1316,1319,1321,1323],{"class":162,"line":296},[270,1310,1311],{"class":279},"points ",[270,1313,302],{"class":275},[270,1315,305],{"class":279},[270,1317,1318],{"class":308},"\"incidents\"",[270,1320,312],{"class":279},[270,1322,65],{"class":315},[270,1324,318],{"class":279},[270,1326,1327,1330,1332,1334,1337,1339,1341],{"class":162,"line":321},[270,1328,1329],{"class":279},"districts ",[270,1331,302],{"class":275},[270,1333,305],{"class":279},[270,1335,1336],{"class":308},"\"districts\"",[270,1338,312],{"class":279},[270,1340,65],{"class":315},[270,1342,318],{"class":279},[270,1344,1345],{"class":162,"line":340},[270,1346,293],{"emptyLinePlaceholder":292},[270,1348,1349,1352,1354],{"class":162,"line":345},[270,1350,1351],{"class":279},"index ",[270,1353,302],{"class":275},[270,1355,1356],{"class":279}," QgsSpatialIndex(\n",[270,1358,1359,1362,1366,1368],{"class":162,"line":359},[270,1360,1361],{"class":279},"    districts.getFeatures(), ",[270,1363,1365],{"class":1364},"s9osk","flags",[270,1367,302],{"class":275},[270,1369,1370],{"class":279},"QgsSpatialIndex.FlagStoreFeatureGeometries\n",[270,1372,1373],{"class":162,"line":364},[270,1374,433],{"class":279},[270,1376,1377,1380,1382,1385,1388,1391,1393,1395,1397],{"class":162,"line":375},[270,1378,1379],{"class":279},"names ",[270,1381,302],{"class":275},[270,1383,1384],{"class":279}," {f.id(): f[",[270,1386,1387],{"class":308},"\"district_name\"",[270,1389,1390],{"class":279},"] ",[270,1392,649],{"class":275},[270,1394,958],{"class":279},[270,1396,395],{"class":275},[270,1398,1399],{"class":279}," districts.getFeatures()}\n",[270,1401,1402],{"class":162,"line":386},[270,1403,293],{"emptyLinePlaceholder":292},[270,1405,1406,1408,1411,1413],{"class":162,"line":401},[270,1407,649],{"class":275},[270,1409,1410],{"class":279}," point ",[270,1412,395],{"class":275},[270,1414,1415],{"class":279}," points.getFeatures():\n",[270,1417,1418,1420,1422],{"class":162,"line":410},[270,1419,661],{"class":279},[270,1421,302],{"class":275},[270,1423,1424],{"class":279}," point.geometry()\n",[270,1426,1427,1430,1432],{"class":162,"line":415},[270,1428,1429],{"class":279},"    assigned ",[270,1431,302],{"class":275},[270,1433,1434],{"class":315}," None\n",[270,1436,1438,1440,1443,1445],{"class":162,"line":1437},14,[270,1439,389],{"class":275},[270,1441,1442],{"class":279}," did ",[270,1444,395],{"class":275},[270,1446,1447],{"class":279}," index.intersects(geometry.boundingBox()):\n",[270,1449,1451,1454],{"class":162,"line":1450},15,[270,1452,1453],{"class":275},"        if",[270,1455,1456],{"class":279}," geometry.within(index.geometry(did)):\n",[270,1458,1460,1463,1465],{"class":162,"line":1459},16,[270,1461,1462],{"class":279},"            assigned ",[270,1464,302],{"class":275},[270,1466,1467],{"class":279}," names[did]\n",[270,1469,1471],{"class":162,"line":1470},17,[270,1472,1473],{"class":275},"            break\n",[270,1475,1477,1479,1482,1485,1488,1491,1494],{"class":162,"line":1476},18,[270,1478,722],{"class":315},[270,1480,1481],{"class":279},"(point[",[270,1483,1484],{"class":308},"\"incident_id\"",[270,1486,1487],{"class":279},"], assigned ",[270,1489,1490],{"class":275},"or",[270,1492,1493],{"class":308}," \"unassigned\"",[270,1495,433],{"class":279},[14,1497,1498,1500,1501,1503,1504,1506,1507,1510,1511,1513,1514,1516],{},[228,1499,438],{}," Using ",[17,1502,580],{}," rather than ",[17,1505,19],{}," means a point sitting exactly on a shared district boundary is assigned to neither district instead of both — which is what you want when the districts tile a surface and each incident must be counted once. The ",[17,1508,1509],{},"break"," stops at the first containing polygon, which is safe precisely because ",[17,1512,580],{}," cannot match two neighbours. If unassigned boundary points are unacceptable, fall back to ",[17,1515,19],{}," for those and pick deterministically, for example the district with the lowest feature ID, so repeated runs agree.",[217,1518,1520],{"id":1519},"qgis-version-compatibility","QGIS version compatibility",[14,1522,1523,1524,1526],{},"The examples target ",[228,1525,230],{}," (Python 3.12).",[537,1528,1529,1542],{},[540,1530,1531],{},[543,1532,1533,1536,1539],{},[546,1534,1535],{},"QGIS version",[546,1537,1538],{},"Python",[546,1540,1541],{},"Notes",[556,1543,1544,1555,1566],{},[543,1545,1546,1549,1552],{},[561,1547,1548],{},"3.28 LTR",[561,1550,1551],{},"3.9",[561,1553,1554],{},"Identical predicate API and semantics.",[543,1556,1557,1560,1563],{},[561,1558,1559],{},"3.34 LTR",[561,1561,1562],{},"3.12",[561,1564,1565],{},"Baseline for this page.",[543,1567,1568,1571,1573],{},[561,1569,1570],{},"3.40 \u002F 3.44",[561,1572,1562],{},[561,1574,1575],{},"Faster GEOS backend for repeated predicates; results unchanged.",[14,1577,1578,1579,1581],{},"The predicate methods on ",[17,1580,251],{}," have been stable throughout 3.x, and their semantics follow the OGC simple-features definitions, so they match PostGIS behaviour as well.",[217,1583,1585],{"id":1584},"troubleshooting","Troubleshooting",[222,1587,1588,1598,1610,1620,1630,1640],{},[225,1589,1590,1593,1594,1597],{},[228,1591,1592],{},"Everything returns false."," The two layers are in different CRSs, so their coordinate ranges do not overlap. Compare ",[17,1595,1596],{},"authid()"," on both.",[225,1599,1600,1603,1604,1606,1607,1609],{},[228,1601,1602],{},"Adjacent parcels are counted twice in a join."," They share a boundary, which ",[17,1605,19],{}," treats as a hit. Use ",[17,1608,608],{}," if a shared edge should not count.",[225,1611,1612,439,1615,40,1617,1619],{},[228,1613,1614],{},"A geometry appears to intersect itself.",[17,1616,580],{},[17,1618,19],{}," are both true for identical geometries. Exclude self-matches by comparing feature IDs in a self-join.",[225,1621,1622,1625,1626,1629],{},[228,1623,1624],{},"A GEOS exception is raised."," One of the inputs is invalid. Repair it with ",[17,1627,1628],{},"makeValid()"," first.",[225,1631,1632,1635,1636,1639],{},[228,1633,1634],{},"The test is slow on large layers."," No bounding-box prefilter. Add ",[17,1637,1638],{},"QgsFeatureRequest().setFilterRect()"," or build a spatial index.",[225,1641,1642,1645,1646,994],{},[228,1643,1644],{},"Results differ from the GUI's spatial join."," The GUI defaults to a specific predicate set; check which predicates were ticked rather than assuming it used ",[17,1647,32],{},[217,1649,1651],{"id":1650},"conclusion","Conclusion",[14,1653,1654,1656],{},[17,1655,19],{}," is the broadest of the spatial predicates: it is true for a shared corner, a shared edge, a partial overlap and full containment alike. Choosing the narrower predicate that matches your actual question makes the code self-documenting and avoids the double-counting that shared boundaries otherwise cause. For performance, prefilter with a bounding box and union the query side when you only need \"any\".",[217,1658,1660],{"id":1659},"frequently-asked-questions","Frequently Asked Questions",[14,1662,1663,1666,1667,1669,1670,1672],{},[228,1664,1665],{},"Do two polygons that share only a border intersect?","\nYes. ",[17,1668,19],{}," is true whenever the geometries share any point, including a single boundary point. If a shared edge should not count, use ",[17,1671,608],{},", which requires the interiors to share area.",[14,1674,1675,1678,1679,1681,1682,1685],{},[228,1676,1677],{},"Does intersects() reproject the geometries?","\nNo. It compares raw coordinates and assumes both are in the same CRS. A mismatch produces ",[17,1680,116],{}," for every pair with no warning, so check ",[17,1683,1684],{},"crs().authid()"," on both layers before testing.",[14,1687,1688,1691],{},[228,1689,1690],{},"Is a bounding-box test enough on its own?","\nNo. It is an approximation that produces false positives — a diagonal line's box covers a large area the line never touches. Use it to shorten the candidate list, then run the exact predicate.",[14,1693,1694,1697,1698,40,1701,1704],{},[228,1695,1696],{},"What is the difference between contains and within?","\nThey are inverses. ",[17,1699,1700],{},"a.contains(b)",[17,1702,1703],{},"b.within(a)"," mean the same thing. Both are true when the geometries are identical, because a shape contains itself.",[14,1706,1707,1710,1711,1714,1715,1717],{},[228,1708,1709],{},"Is disjoint just the opposite of intersects?","\nYes, exactly — ",[17,1712,1713],{},"a.disjoint(b)"," is always the negation of ",[17,1716,255],{},". Use whichever reads more naturally in the surrounding code.",[217,1719,1721],{"id":1720},"related","Related",[222,1723,1724,1729,1733,1739],{},[225,1725,1726,1728],{},[25,1727,28],{"href":27}," — the guide this recipe belongs to",[225,1730,1731],{},[25,1732,993],{"href":992},[225,1734,1735],{},[25,1736,1738],{"href":1737},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcalculate-distance-between-features-pyqgis\u002F","Calculate the Distance Between Features in PyQGIS",[225,1740,1741],{},[25,1742,1744],{"href":1743},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","Clip a Vector Layer in PyQGIS",[1746,1747,1748],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":266,"searchDepth":289,"depth":289,"links":1750},[1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761],{"id":219,"depth":289,"text":220},{"id":244,"depth":289,"text":245},{"id":529,"depth":289,"text":530},{"id":744,"depth":289,"text":745},{"id":997,"depth":289,"text":998},{"id":1162,"depth":289,"text":1163},{"id":1519,"depth":289,"text":1520},{"id":1584,"depth":289,"text":1585},{"id":1650,"depth":289,"text":1651},{"id":1659,"depth":289,"text":1660},{"id":1720,"depth":289,"text":1721},"Use QgsGeometry.intersects and related predicates in PyQGIS, understand boundary-touch behaviour, prefilter with bounding boxes, and pick the right predicate per question.","md",{"slug":1765,"type":1766,"breadcrumb":1767,"datePublished":1768,"dateModified":1768},"check-geometry-intersects-pyqgis","article","Test for Intersection","2026-08-01","\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis",{"title":5,"description":1762},"spatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fcheck-geometry-intersects-pyqgis\u002Findex","rmAQRJygeT9l1PyL3fkLt68v6WPT4z9Zvac0kZ9SVeA",1785585990477]