We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use clean_address function to join multiple free text lines together
The `clean_address` method added in #7568 allows a standardised approach to taking messy ordered multiple line address strings (of any type of composition) and joining them together into a single string.

We can now use `clean_address` to replace the many variants throughout spiders of attempting to join these multi-line address strings. An added benefit is being able to quickly find where multi-line address strings are parsed (via searching for `clean_address` instances), making it easier to change address handling in the future.

Related to #5598

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `locations/spiders/zizzi_gb.py`
Content:
```
1 import scrapy
2 
3 from locations.dict_parser import DictParser
4 
5 
6 class ZizziGBSpider(scrapy.Spider):
7     name = "zizzi_gb"
8     item_attributes = {"brand": "Zizzi", "brand_wikidata": "Q8072944"}
9     start_urls = ["https://www.zizzi.co.uk/wp-json/locations/get_venues"]
10 
11     def parse(self, response):
12         for store in response.json()["data"]:
13             item = DictParser.parse(store)
14             item["addr_full"] = ", ".join(store["address"].split("\r\n"))
15             item["image"] = store["featured_image"]
16             item["website"] = store["link"]
17 
18             if store["region"] == "Ireland":
19                 item.pop("state")
20                 item["country"] = "IE"
21             else:
22                 item["country"] = "GB"
23 
24             yield item
25
```

Path: `locations/spiders/zambrero_au.py`
Content:
```
1 import re
2 
3 from scrapy import Spider
4 from scrapy.http import Request
5 
6 from locations.categories import Categories
7 from locations.hours import OpeningHours
8 from locations.items import Feature
9 
10 
11 class ZambreroAUSpider(Spider):
12     name = "zambrero_au"
13     item_attributes = {"brand": "Zambrero", "brand_wikidata": "Q18636431", "extras": Categories.FAST_FOOD.value}
14     allowed_domains = ["www.zambrero.com.au"]
15 
16     def start_requests(self):
17         yield Request(url=f"https://{self.allowed_domains[0]}/locations", callback=self.parse_location_list)
18 
19     def parse_location_list(self, response):
20         location_urls = response.xpath('//div[@data-location-id]//a[@title="Order & Store Info"]/@href').getall()
21         for location_url in location_urls:
22             yield Request(url=location_url, callback=self.parse_location)
23 
24     def parse_location(self, response):
25         properties = {
26             "ref": response.xpath("//@data-location-id").get(),
27             "name": re.sub(r"\s+", " ", response.xpath("//div[@data-location-id]/h4/text()").get()).strip(),
28             "lat": response.xpath("//@data-lat").get(),
29             "lon": response.xpath("///@data-lng").get(),
30             "addr_full": re.sub(
31                 r"\s+",
32                 " ",
33                 " ".join(response.xpath('//div[@data-location-id]//span[contains(@class, "address")]/text()').getall()),
34             ).strip(),
35             "phone": response.xpath('//a[contains(@class, "phone")]/@href').get().replace("tel:", ""),
36             "email": response.xpath('//a[contains(@href, "mailto:")]/@href').get().replace("mailto:", ""),
37             "website": response.url,
38             "opening_hours": OpeningHours(),
39         }
40         if "Temporarily Closed" in properties["name"]:
41             return
42         if properties["phone"] == "0":
43             properties.pop("phone")
44 
45         hours_text = re.sub(
46             r"\s+", " ", " ".join(response.xpath('//div[contains(@class, "hours-item")]/span/text()').getall())
47         )
48         properties["opening_hours"].add_ranges_from_string(hours_text)
49 
50         # Some store names and URLs contain "Opening Soon" but numerous of
51         # these are already open and the URL hasn't been changed. A more
52         # reliable way of knowing a store is not yet open is that it has
53         # no opening hours specified.
54         if not properties["opening_hours"].as_opening_hours():
55             return
56 
57         yield Feature(**properties)
58
```

Path: `locations/spiders/woolworths_au.py`
Content:
```
1 import scrapy
2 
3 from locations.dict_parser import DictParser
4 
5 
6 class WoolworthsAUSpider(scrapy.Spider):
7     name = "woolworths_au"
8     item_attributes = {"brand": "Woolworths", "brand_wikidata": "Q3249145"}
9     allowed_domains = ["woolworths.com.au"]
10     start_urls = [
11         "https://www.woolworths.com.au/apis/ui/StoreLocator/Stores?Max=10000&Division=SUPERMARKETS,PETROL,CALTEXWOW,AMPOLMETRO,AMPOL&Facility=&postcode=*"
12     ]
13     custom_settings = {"ROBOTSTXT_OBEY": False}
14     requires_proxy = "AU"
15 
16     def parse(self, response):
17         data = response.json()
18 
19         for i in data["Stores"]:
20             if not i["IsOpen"]:
21                 continue
22 
23             i["street_address"] = ", ".join(filter(None, [i["AddressLine1"], i["AddressLine2"]]))
24             i["ref"] = i.pop("StoreNo")
25             i["city"] = i.pop("Suburb")
26 
27             item = DictParser.parse(i)
28 
29             item["website"] = (
30                 "https://www.woolworths.com.au/shop/storelocator/"
31                 + "-".join([item["state"], item["city"], item["ref"], i["Division"]]).lower()
32             )
33 
34             # TODO: types needs some work, NSI seems out of date too
35             item["extras"] = {"type": i["Division"]}
36 
37             yield item
38
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/locations/spiders/woolworths_au.py b/locations/spiders/woolworths_au.py
--- a/locations/spiders/woolworths_au.py
+++ b/locations/spiders/woolworths_au.py
@@ -1,6 +1,7 @@
 import scrapy
 
 from locations.dict_parser import DictParser
+from locations.pipelines.address_clean_up import clean_address
 
 
 class WoolworthsAUSpider(scrapy.Spider):
@@ -20,7 +21,7 @@
             if not i["IsOpen"]:
                 continue
 
-            i["street_address"] = ", ".join(filter(None, [i["AddressLine1"], i["AddressLine2"]]))
+            i["street_address"] = clean_address([i["AddressLine1"], i["AddressLine2"]])
             i["ref"] = i.pop("StoreNo")
             i["city"] = i.pop("Suburb")
 
diff --git a/locations/spiders/zambrero_au.py b/locations/spiders/zambrero_au.py
--- a/locations/spiders/zambrero_au.py
+++ b/locations/spiders/zambrero_au.py
@@ -6,6 +6,7 @@
 from locations.categories import Categories
 from locations.hours import OpeningHours
 from locations.items import Feature
+from locations.pipelines.address_clean_up import clean_address
 
 
 class ZambreroAUSpider(Spider):
@@ -27,11 +28,9 @@
             "name": re.sub(r"\s+", " ", response.xpath("//div[@data-location-id]/h4/text()").get()).strip(),
             "lat": response.xpath("//@data-lat").get(),
             "lon": response.xpath("///@data-lng").get(),
-            "addr_full": re.sub(
-                r"\s+",
-                " ",
-                " ".join(response.xpath('//div[@data-location-id]//span[contains(@class, "address")]/text()').getall()),
-            ).strip(),
+            "addr_full": clean_address(
+                " ".join(response.xpath('//div[@data-location-id]//span[contains(@class, "address")]/text()').getall())
+            ),
             "phone": response.xpath('//a[contains(@class, "phone")]/@href').get().replace("tel:", ""),
             "email": response.xpath('//a[contains(@href, "mailto:")]/@href').get().replace("mailto:", ""),
             "website": response.url,
diff --git a/locations/spiders/zizzi_gb.py b/locations/spiders/zizzi_gb.py
--- a/locations/spiders/zizzi_gb.py
+++ b/locations/spiders/zizzi_gb.py
@@ -1,6 +1,7 @@
 import scrapy
 
 from locations.dict_parser import DictParser
+from locations.pipelines.address_clean_up import clean_address
 
 
 class ZizziGBSpider(scrapy.Spider):
@@ -11,7 +12,7 @@
     def parse(self, response):
         for store in response.json()["data"]:
             item = DictParser.parse(store)
-            item["addr_full"] = ", ".join(store["address"].split("\r\n"))
+            item["addr_full"] = clean_address(store["address"].split("\r\n"))
             item["image"] = store["featured_image"]
             item["website"] = store["link"]

{"golden_diff": "diff --git a/locations/spiders/woolworths_au.py b/locations/spiders/woolworths_au.py\n--- a/locations/spiders/woolworths_au.py\n+++ b/locations/spiders/woolworths_au.py\n@@ -1,6 +1,7 @@\n import scrapy\n \n from locations.dict_parser import DictParser\n+from locations.pipelines.address_clean_up import clean_address\n \n \n class WoolworthsAUSpider(scrapy.Spider):\n@@ -20,7 +21,7 @@\n             if not i[\"IsOpen\"]:\n                 continue\n \n-            i[\"street_address\"] = \", \".join(filter(None, [i[\"AddressLine1\"], i[\"AddressLine2\"]]))\n+            i[\"street_address\"] = clean_address([i[\"AddressLine1\"], i[\"AddressLine2\"]])\n             i[\"ref\"] = i.pop(\"StoreNo\")\n             i[\"city\"] = i.pop(\"Suburb\")\n \ndiff --git a/locations/spiders/zambrero_au.py b/locations/spiders/zambrero_au.py\n--- a/locations/spiders/zambrero_au.py\n+++ b/locations/spiders/zambrero_au.py\n@@ -6,6 +6,7 @@\n from locations.categories import Categories\n from locations.hours import OpeningHours\n from locations.items import Feature\n+from locations.pipelines.address_clean_up import clean_address\n \n \n class ZambreroAUSpider(Spider):\n@@ -27,11 +28,9 @@\n             \"name\": re.sub(r\"\\s+\", \" \", response.xpath(\"//div[@data-location-id]/h4/text()\").get()).strip(),\n             \"lat\": response.xpath(\"//@data-lat\").get(),\n             \"lon\": response.xpath(\"///@data-lng\").get(),\n-            \"addr_full\": re.sub(\n-                r\"\\s+\",\n-                \" \",\n-                \" \".join(response.xpath('//div[@data-location-id]//span[contains(@class, \"address\")]/text()').getall()),\n-            ).strip(),\n+            \"addr_full\": clean_address(\n+                \" \".join(response.xpath('//div[@data-location-id]//span[contains(@class, \"address\")]/text()').getall())\n+            ),\n             \"phone\": response.xpath('//a[contains(@class, \"phone\")]/@href').get().replace(\"tel:\", \"\"),\n             \"email\": response.xpath('//a[contains(@href, \"mailto:\")]/@href').get().replace(\"mailto:\", \"\"),\n             \"website\": response.url,\ndiff --git a/locations/spiders/zizzi_gb.py b/locations/spiders/zizzi_gb.py\n--- a/locations/spiders/zizzi_gb.py\n+++ b/locations/spiders/zizzi_gb.py\n@@ -1,6 +1,7 @@\n import scrapy\n \n from locations.dict_parser import DictParser\n+from locations.pipelines.address_clean_up import clean_address\n \n \n class ZizziGBSpider(scrapy.Spider):\n@@ -11,7 +12,7 @@\n     def parse(self, response):\n         for store in response.json()[\"data\"]:\n             item = DictParser.parse(store)\n-            item[\"addr_full\"] = \", \".join(store[\"address\"].split(\"\\r\\n\"))\n+            item[\"addr_full\"] = clean_address(store[\"address\"].split(\"\\r\\n\"))\n             item[\"image\"] = store[\"featured_image\"]\n             item[\"website\"] = store[\"link\"]\n", "issue": "Use clean_address function to join multiple free text lines together\nThe `clean_address` method added in #7568 allows a standardised approach to taking messy ordered multiple line address strings (of any type of composition) and joining them together into a single string.\r\n\r\nWe can now use `clean_address` to replace the many variants throughout spiders of attempting to join these multi-line address strings. An added benefit is being able to quickly find where multi-line address strings are parsed (via searching for `clean_address` instances), making it easier to change address handling in the future.\r\n\r\nRelated to #5598\n", "before_files": [{"content": "import scrapy\n\nfrom locations.dict_parser import DictParser\n\n\nclass ZizziGBSpider(scrapy.Spider):\n    name = \"zizzi_gb\"\n    item_attributes = {\"brand\": \"Zizzi\", \"brand_wikidata\": \"Q8072944\"}\n    start_urls = [\"https://www.zizzi.co.uk/wp-json/locations/get_venues\"]\n\n    def parse(self, response):\n        for store in response.json()[\"data\"]:\n            item = DictParser.parse(store)\n            item[\"addr_full\"] = \", \".join(store[\"address\"].split(\"\\r\\n\"))\n            item[\"image\"] = store[\"featured_image\"]\n            item[\"website\"] = store[\"link\"]\n\n            if store[\"region\"] == \"Ireland\":\n                item.pop(\"state\")\n                item[\"country\"] = \"IE\"\n            else:\n                item[\"country\"] = \"GB\"\n\n            yield item\n", "path": "locations/spiders/zizzi_gb.py"}, {"content": "import re\n\nfrom scrapy import Spider\nfrom scrapy.http import Request\n\nfrom locations.categories import Categories\nfrom locations.hours import OpeningHours\nfrom locations.items import Feature\n\n\nclass ZambreroAUSpider(Spider):\n    name = \"zambrero_au\"\n    item_attributes = {\"brand\": \"Zambrero\", \"brand_wikidata\": \"Q18636431\", \"extras\": Categories.FAST_FOOD.value}\n    allowed_domains = [\"www.zambrero.com.au\"]\n\n    def start_requests(self):\n        yield Request(url=f\"https://{self.allowed_domains[0]}/locations\", callback=self.parse_location_list)\n\n    def parse_location_list(self, response):\n        location_urls = response.xpath('//div[@data-location-id]//a[@title=\"Order & Store Info\"]/@href').getall()\n        for location_url in location_urls:\n            yield Request(url=location_url, callback=self.parse_location)\n\n    def parse_location(self, response):\n        properties = {\n            \"ref\": response.xpath(\"//@data-location-id\").get(),\n            \"name\": re.sub(r\"\\s+\", \" \", response.xpath(\"//div[@data-location-id]/h4/text()\").get()).strip(),\n            \"lat\": response.xpath(\"//@data-lat\").get(),\n            \"lon\": response.xpath(\"///@data-lng\").get(),\n            \"addr_full\": re.sub(\n                r\"\\s+\",\n                \" \",\n                \" \".join(response.xpath('//div[@data-location-id]//span[contains(@class, \"address\")]/text()').getall()),\n            ).strip(),\n            \"phone\": response.xpath('//a[contains(@class, \"phone\")]/@href').get().replace(\"tel:\", \"\"),\n            \"email\": response.xpath('//a[contains(@href, \"mailto:\")]/@href').get().replace(\"mailto:\", \"\"),\n            \"website\": response.url,\n            \"opening_hours\": OpeningHours(),\n        }\n        if \"Temporarily Closed\" in properties[\"name\"]:\n            return\n        if properties[\"phone\"] == \"0\":\n            properties.pop(\"phone\")\n\n        hours_text = re.sub(\n            r\"\\s+\", \" \", \" \".join(response.xpath('//div[contains(@class, \"hours-item\")]/span/text()').getall())\n        )\n        properties[\"opening_hours\"].add_ranges_from_string(hours_text)\n\n        # Some store names and URLs contain \"Opening Soon\" but numerous of\n        # these are already open and the URL hasn't been changed. A more\n        # reliable way of knowing a store is not yet open is that it has\n        # no opening hours specified.\n        if not properties[\"opening_hours\"].as_opening_hours():\n            return\n\n        yield Feature(**properties)\n", "path": "locations/spiders/zambrero_au.py"}, {"content": "import scrapy\n\nfrom locations.dict_parser import DictParser\n\n\nclass WoolworthsAUSpider(scrapy.Spider):\n    name = \"woolworths_au\"\n    item_attributes = {\"brand\": \"Woolworths\", \"brand_wikidata\": \"Q3249145\"}\n    allowed_domains = [\"woolworths.com.au\"]\n    start_urls = [\n        \"https://www.woolworths.com.au/apis/ui/StoreLocator/Stores?Max=10000&Division=SUPERMARKETS,PETROL,CALTEXWOW,AMPOLMETRO,AMPOL&Facility=&postcode=*\"\n    ]\n    custom_settings = {\"ROBOTSTXT_OBEY\": False}\n    requires_proxy = \"AU\"\n\n    def parse(self, response):\n        data = response.json()\n\n        for i in data[\"Stores\"]:\n            if not i[\"IsOpen\"]:\n                continue\n\n            i[\"street_address\"] = \", \".join(filter(None, [i[\"AddressLine1\"], i[\"AddressLine2\"]]))\n            i[\"ref\"] = i.pop(\"StoreNo\")\n            i[\"city\"] = i.pop(\"Suburb\")\n\n            item = DictParser.parse(i)\n\n            item[\"website\"] = (\n                \"https://www.woolworths.com.au/shop/storelocator/\"\n                + \"-\".join([item[\"state\"], item[\"city\"], item[\"ref\"], i[\"Division\"]]).lower()\n            )\n\n            # TODO: types needs some work, NSI seems out of date too\n            item[\"extras\"] = {\"type\": i[\"Division\"]}\n\n            yield item\n", "path": "locations/spiders/woolworths_au.py"}], "after_files": [{"content": "import scrapy\n\nfrom locations.dict_parser import DictParser\nfrom locations.pipelines.address_clean_up import clean_address\n\n\nclass ZizziGBSpider(scrapy.Spider):\n    name = \"zizzi_gb\"\n    item_attributes = {\"brand\": \"Zizzi\", \"brand_wikidata\": \"Q8072944\"}\n    start_urls = [\"https://www.zizzi.co.uk/wp-json/locations/get_venues\"]\n\n    def parse(self, response):\n        for store in response.json()[\"data\"]:\n            item = DictParser.parse(store)\n            item[\"addr_full\"] = clean_address(store[\"address\"].split(\"\\r\\n\"))\n            item[\"image\"] = store[\"featured_image\"]\n            item[\"website\"] = store[\"link\"]\n\n            if store[\"region\"] == \"Ireland\":\n                item.pop(\"state\")\n                item[\"country\"] = \"IE\"\n            else:\n                item[\"country\"] = \"GB\"\n\n            yield item\n", "path": "locations/spiders/zizzi_gb.py"}, {"content": "import re\n\nfrom scrapy import Spider\nfrom scrapy.http import Request\n\nfrom locations.categories import Categories\nfrom locations.hours import OpeningHours\nfrom locations.items import Feature\nfrom locations.pipelines.address_clean_up import clean_address\n\n\nclass ZambreroAUSpider(Spider):\n    name = \"zambrero_au\"\n    item_attributes = {\"brand\": \"Zambrero\", \"brand_wikidata\": \"Q18636431\", \"extras\": Categories.FAST_FOOD.value}\n    allowed_domains = [\"www.zambrero.com.au\"]\n\n    def start_requests(self):\n        yield Request(url=f\"https://{self.allowed_domains[0]}/locations\", callback=self.parse_location_list)\n\n    def parse_location_list(self, response):\n        location_urls = response.xpath('//div[@data-location-id]//a[@title=\"Order & Store Info\"]/@href').getall()\n        for location_url in location_urls:\n            yield Request(url=location_url, callback=self.parse_location)\n\n    def parse_location(self, response):\n        properties = {\n            \"ref\": response.xpath(\"//@data-location-id\").get(),\n            \"name\": re.sub(r\"\\s+\", \" \", response.xpath(\"//div[@data-location-id]/h4/text()\").get()).strip(),\n            \"lat\": response.xpath(\"//@data-lat\").get(),\n            \"lon\": response.xpath(\"///@data-lng\").get(),\n            \"addr_full\": clean_address(\n                \" \".join(response.xpath('//div[@data-location-id]//span[contains(@class, \"address\")]/text()').getall())\n            ),\n            \"phone\": response.xpath('//a[contains(@class, \"phone\")]/@href').get().replace(\"tel:\", \"\"),\n            \"email\": response.xpath('//a[contains(@href, \"mailto:\")]/@href').get().replace(\"mailto:\", \"\"),\n            \"website\": response.url,\n            \"opening_hours\": OpeningHours(),\n        }\n        if \"Temporarily Closed\" in properties[\"name\"]:\n            return\n        if properties[\"phone\"] == \"0\":\n            properties.pop(\"phone\")\n\n        hours_text = re.sub(\n            r\"\\s+\", \" \", \" \".join(response.xpath('//div[contains(@class, \"hours-item\")]/span/text()').getall())\n        )\n        properties[\"opening_hours\"].add_ranges_from_string(hours_text)\n\n        # Some store names and URLs contain \"Opening Soon\" but numerous of\n        # these are already open and the URL hasn't been changed. A more\n        # reliable way of knowing a store is not yet open is that it has\n        # no opening hours specified.\n        if not properties[\"opening_hours\"].as_opening_hours():\n            return\n\n        yield Feature(**properties)\n", "path": "locations/spiders/zambrero_au.py"}, {"content": "import scrapy\n\nfrom locations.dict_parser import DictParser\nfrom locations.pipelines.address_clean_up import clean_address\n\n\nclass WoolworthsAUSpider(scrapy.Spider):\n    name = \"woolworths_au\"\n    item_attributes = {\"brand\": \"Woolworths\", \"brand_wikidata\": \"Q3249145\"}\n    allowed_domains = [\"woolworths.com.au\"]\n    start_urls = [\n        \"https://www.woolworths.com.au/apis/ui/StoreLocator/Stores?Max=10000&Division=SUPERMARKETS,PETROL,CALTEXWOW,AMPOLMETRO,AMPOL&Facility=&postcode=*\"\n    ]\n    custom_settings = {\"ROBOTSTXT_OBEY\": False}\n    requires_proxy = \"AU\"\n\n    def parse(self, response):\n        data = response.json()\n\n        for i in data[\"Stores\"]:\n            if not i[\"IsOpen\"]:\n                continue\n\n            i[\"street_address\"] = clean_address([i[\"AddressLine1\"], i[\"AddressLine2\"]])\n            i[\"ref\"] = i.pop(\"StoreNo\")\n            i[\"city\"] = i.pop(\"Suburb\")\n\n            item = DictParser.parse(i)\n\n            item[\"website\"] = (\n                \"https://www.woolworths.com.au/shop/storelocator/\"\n                + \"-\".join([item[\"state\"], item[\"city\"], item[\"ref\"], i[\"Division\"]]).lower()\n            )\n\n            # TODO: types needs some work, NSI seems out of date too\n            item[\"extras\"] = {\"type\": i[\"Division\"]}\n\n            yield item\n", "path": "locations/spiders/woolworths_au.py"}]}