Skip to content
pageinspection

LocalBusiness schema: address, hours and the subtype that fits

Two properties carry this type: the address someone drives to and the hours they arrive during. Both are simple to write and both are wrong on a startling number of sites, usually because a template rendered the easy case and nobody checked what happens to a kitchen that closes at one in the morning.

The short answer

Pick the narrowest LocalBusiness subtype that is still accurate, and give each location its own page and its own block. The hours block is where implementations break: overnight hours crossing midnight, split shifts, seasonal closures and 24-hour operation each have a specific form, and guessing produces hours that validate and are wrong.

ImportantAudit check · LocalBusiness Schema

Choosing the narrowest subtype that is still true

LocalBusiness has more descendants than any other branch of schema.org that a normal site will touch. Store, Restaurant, Bakery, Dentist, Attorney, Plumber, DryCleaningOrLaundry, SelfStorage, on and on. It is a subtype of Organization, which is what stops you needing both, and the parent type explains why declaring both usually backfires.

Picking the specific one buys you two things and neither is a ranking effect. The first is type-specific properties: servesCuisine and menu exist on the food types and nowhere else, medicalSpecialty exists on the medical ones. Declare LocalBusiness and those properties become invalid on your own block. The second is simply that a narrower type is a stronger statement. A consumer that has never heard of Bakery falls back on LocalBusiness, because inheritance means the general claim is still there.

Rules for choosing. If a subtype describes the business exactly, use it. If nothing fits, plain LocalBusiness is the right answer, not the closest-sounding specific type: a workshop is not a Store because both have a door. And avoid ProfessionalService, which carries a note on schema.org itself warning that the type became confused with Service and listing the concrete types it used to stand in for. If you are a solicitor or an accountant, the concrete type is what you want.

The hours block, and the four cases people get wrong

openingHoursSpecification is an array of objects, each of which says which days it applies to and when the doors open and shut. The easy case, nine to five on weekdays, is one object and everybody gets it right. Then reality arrives.

opening-hours.json
"openingHoursSpecification": [
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Tuesday", "Wednesday", "Thursday"],
    "opens": "12:00",
    "closes": "15:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Tuesday", "Wednesday", "Thursday"],
    "opens": "18:00",
    "closes": "23:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Friday", "Saturday"],
    "opens": "18:00",
    "closes": "01:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": "Sunday",
    "opens": "00:00",
    "closes": "00:00"
  },
  {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": "Monday",
    "opens": "12:00",
    "closes": "22:00",
    "validFrom": "2026-06-01",
    "validThrough": "2026-08-31"
  }
]

Reading that back, in the order the awkwardness usually turns up.

  • A split shift is two objects, not one. Lunch and dinner service on the same day means two entries naming the same days. There is no way to express a gap inside a single specification, and a restaurant that declares 12:00 to 23:00 has just told every consumer it serves food at four in the afternoon.
  • Overnight is a closing time before the opening. The Friday entry closes at 01:00, which is earlier than 18:00, and that is how you say the following morning. Splitting it into a Friday evening entry and a Saturday small-hours entry is the intuitive move and it produces a Saturday that opens at midnight for an hour and then shuts.
  • Seasonal hours need the two date bounds. validFrom and validThrough scope an entry to a range. Without them, the summer opening you added in June is still being asserted in November, and hardcoding it means somebody has to remember to edit the template.
  • Days you are shut are worth stating. Omitting Sunday leaves the day undefined rather than closed, and undefined is what a consumer fills in with a guess or with data from somewhere else.

Closed all day and open all day are one minute apart.

Google's convention for a day you are shut is opens and closes both set to 00:00. Its convention for twenty-four hour trading is 00:00 to 23:59. Those two are visually near identical, they sit in the same array, and one says the opposite of the other. A generator that rounds 23:59 up to midnight because it looks tidier converts every one of your all-night locations into a closed one, silently, and the block still validates.

One more thing to know: openingHours also exists, as a string property in the old shorthand, Mo-Fr 09:00-17:00. It is not deprecated and it cannot express any of the four cases above. Use the specification objects and do not ship both, because two descriptions of your hours that disagree is worse than one that is merely simplistic.

Check one page for a location claim

This reports whether the URL declares a type in the local-business family. What it cannot do is confirm the address in the block matches the address on your other listings, which is a comparison across sources rather than a property of the page.

Check one page for LocalBusiness markup

No signup required. Each free search audits one page, paste any URL to see it in action.

One location, one page, one block

The modelling question that decides whether a multi-site business gets this right. Five branches do not go in an array on the homepage. Each branch gets its own page, and that page carries one block describing that branch, with its own identifier, its own coordinates and its own hours.

The reason is not stylistic. A block is a description of the page it sits on. Five locations on your homepage make the homepage a claim about five addresses at once, and there is nothing for a consumer to link a specific address to, because no URL corresponds to any of them. Branch pages give every address a page to be the subject of.

location-page.html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Restaurant",
  "@id": "https://example.com/locations/shoreditch#place",
  "name": "Kiln and Co Shoreditch",
  "url": "https://example.com/locations/shoreditch",
  "parentOrganization": { "@id": "https://example.com/#organization" },
  "telephone": "+44 20 7946 0123",
  "priceRange": "££",
  "servesCuisine": "Turkish",
  "image": "https://example.com/img/shoreditch-frontage.jpg",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "14 Rivington Street",
    "addressLocality": "London",
    "addressRegion": "Greater London",
    "postalCode": "EC2A 3DU",
    "addressCountry": "GB"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 51.5266,
    "longitude": -0.0803
  },
  "openingHoursSpecification": [
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Wednesday", "Thursday"],
      "opens": "18:00",
      "closes": "23:00"
    },
    {
      "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Friday", "Saturday"],
      "opens": "18:00",
      "closes": "01:00"
    }
  ]
}
</script>

Details in there worth copying. addressCountry takes a two-letter country code, not a country name. geo is decimal degrees as numbers, and a location pinned to the centre of the wrong city is the most embarrassing failure available in this type, because everything else on the page is correct. parentOrganization is what ties the branch to the brand without restating it. And if a branch contains something with different hours, a pharmacy counter inside a shop, that goes in department as a nested business with its own hours rather than a second top-level block.

Your listings outrank your markup

Uncomfortable but true, and it changes where you should spend an afternoon. For anything with a physical presence, the record that drives the map results is a business profile held by the platform, corroborated by directories, review sites and citations. Your JSON-LD is one voice in that chorus, and not the loudest one.

Which is why name, address and phone consistency matters more than the quality of your markup. If your page says Suite 4 and your business profile says Unit 4, and two directories say each, you have not supplied an address, you have supplied a disagreement. The disagreement gets resolved by weight of evidence, and one of the readings that loses might be the true one.

Practical order of work: fix the profile, then the citations that are wrong, then make the markup agree with both. Doing the markup first means you have carefully encoded whichever version of the address happened to be in your CMS.

Why a physical address only rates Important

Two reasons, and the first is that this check is conditional in a way most are not. Most sites should never emit this type at all. A software company with three remote employees and a registered office it never staffs is right to leave it out, and a missing block there is not a defect to be fixed but a correct absence.

The second is what the section above describes. Where the type does apply, the authoritative record lives outside your site, so the markup is corroborating a claim rather than making it. Absence costs you very little if your profile is accurate. That is the ceiling on the rating.

It stays above the bottom of the scale because the failure mode is not a lost opportunity but a wrong instruction: an address or a set of hours that sends somebody to a locked door on a Sunday is worse than saying nothing, and it is the only markup on your site a customer can physically act on. Every rating on the check catalogue is set against that kind of consequence.

Confirming the hours you published are the hours you meant

Parse the block first, then do the one test that actually catches the errors in this type: read your own hours back out of the JSON, for a specific day, without looking at the page. Pick the Friday, work out what time you close, and see whether the answer is the truth. Then do it for a day you are shut and for a day in the season you scoped.

Then check three things no validator will tell you. That the hours in the block match the hours printed on the page, because a store locator that renders its opening times from an API after hydration frequently leaves the JSON-LD to be built by a separate, staler code path. That a curl of the location URL contains the script tag at all, for the same reason. And that the address in the block is character for character the address on your business profile.

If you change hours seasonally, put a reminder in the same calendar as the change. This is the one check on your site with an expiry date built into it, and nothing on the page will look broken the day it lapses.

Questions this check raises

Which LocalBusiness subtype should I use?
The most specific one that is true. Restaurant rather than FoodEstablishment, Dentist rather than MedicalBusiness. If no subtype fits cleanly, LocalBusiness itself is a correct answer, and it is better than a narrower type that misdescribes the business.
How do I mark up hours that cross midnight?
As opens and closes values that span it, for example opens 22:00 and closes 02:00 on the same day entry. Splitting it into two entries produces a business that appears to close at midnight and reopen immediately, which is technically valid and reads wrongly everywhere it is consumed.
Does LocalBusiness markup replace a Google Business Profile?
No. The profile is the authoritative source for local results, and where the two disagree the profile wins. The markup corroborates it and helps everything that is not Google, which is an increasing share of where people ask local questions.